diff --git a/.flocks/flockshub/plugins/tools/api/onesig_v2_5_3_D20250710/_provider.yaml b/.flocks/flockshub/plugins/tools/api/onesig_v2_5_3_D20250710/_provider.yaml deleted file mode 100644 index 092522873..000000000 --- a/.flocks/flockshub/plugins/tools/api/onesig_v2_5_3_D20250710/_provider.yaml +++ /dev/null @@ -1,158 +0,0 @@ -name: onesig_v2_5_3_D20250710 -service_id: onesig_v2_5_3_D20250710_api -version: "2.5.3 D20250710" -description: > - OneSIG (Secure Internet Gateway) Web API service — *older v2.5 firmware* - variant. Authentication still uses cookie session, but the login endpoint - receives the **plaintext** password directly (no `GET /v3/pubkey` and no - RSA-OAEP encryption on `POST /v3/login`). All other behaviour mirrors the - standard `onesig` plugin: cookie persistence, captcha / TOTP fallback, - session auto-relogin, and RSA-OAEP encryption of sensitive write fields - (change_password, user_create / user_delete, audit-log purge, interface - password, device upgrade password, …). - - Configure host base URL, username and password separately in the credentials - form. The optional captcha / TOTP fields are only required when the device - has captcha or two-factor authentication enabled. -description_cn: > - OneSIG(安全互联网网关)Web API 服务 —— *老版本 v2.5 固件* 变体。认证仍走 - Cookie 会话,但登录接口直接发送**明文**密码(不再调 `GET /v3/pubkey`,不对 - `POST /v3/login` 的 password 字段做 RSA-OAEP 加密)。其余行为与标准 `onesig` - 插件保持一致:Cookie 持久化、验证码 / TOTP 兜底、会话自动重登,以及对*非登录* - 敏感写字段(改密、删用户、删审计、接口启停、设备升级口令等)的 RSA-OAEP 加密。 - - 配置页需分别填写 Base URL、用户名、密码;当设备开启图形验证码或 TOTP 时, - 对应字段可在调用 `onesig_v2_5_3_D20250710_login` 时按需传入。 -auth: - type: custom - secret: onesig_v2_5_3_D20250710_password -credential_fields: - - key: base_url - label: Base URL - storage: config - config_key: base_url - input_type: url - required: true - placeholder: "https://device.example.com" - - key: api_prefix - label: API Prefix - storage: config - config_key: api_prefix - input_type: text - required: false - default: "" - placeholder: "留空(默认)或 /api" - - key: username - label: Username - storage: config - config_key: username - input_type: text - required: true - - key: password - label: Password (plaintext on login) - storage: secret - config_key: password - secret_id: onesig_v2_5_3_D20250710_password - input_type: password - required: true - - key: oaep_hash - label: RSA-OAEP Hash (sensitive write fields only) - storage: config - config_key: oaep_hash - input_type: text - required: false - default: "sha1" - placeholder: "sha1 或 sha256" -defaults: - api_prefix: "" - oaep_hash: "sha1" - timeout: 60 - category: custom - product_version: "2.5.3 D20250710" -notes: | - OneSIG v2.5 老版本(onesig_v2_5_3_D20250710)使用 Cookie 会话鉴权 + 明文密码登录。 - - 常见配置示例: - - api_services.onesig_v2_5_3_D20250710_api.base_url = "https://device.example.com" - - api_services.onesig_v2_5_3_D20250710_api.api_prefix = "" # 大部分 v2.5.x 部署留空;reverse-proxy 部署才填 "/api" - - api_services.onesig_v2_5_3_D20250710_api.username = "admin" - - api_services.onesig_v2_5_3_D20250710_api.password = "{secret:onesig_v2_5_3_D20250710_password}" - - api_services.onesig_v2_5_3_D20250710_api.oaep_hash = "sha1" # 仅用于「敏感写字段」加密;登录与该项无关 - - 与新版 `onesig` 插件的差异(仅一处): - - 登录链路:本插件 **跳过** `GET /v3/pubkey` 与 RSA-OAEP 加密, - `POST /v3/login` 的 body 形如: - - { "username": "admin", "password": "<明文密码>", "captcha"?: "...", "checksum"?: "..." } - - 老固件早期发布版本只接受明文密码,发送 RSA 密文会被解析成乱码并返回 - `responseCode=1009 / 1017`。 - - 其他链路与新版完全一致(已沿用同一份 handler 代码): - - Cookie 会话维持 + `.secret.json` 持久化(key 形如 - `onesig_v2_5_3_D20250710_session_cookie__`); - - 401 / responseCode 1019..1022 自动 force-relogin; - - 改密、删用户、清审计、接口启停、设备升级 等接口里的 `password` / - `oldPassword` / `newPassword` / `dupPassword` 仍由处理器在发送前 - 用最新公钥(每次重新拉 `/v3/pubkey`)做 RSA-OAEP 加密;调用方继续 - 传入**明文**即可,请勿手动加密。 - - `api_prefix` 选取建议(与新版相同): - - 默认值 `""`(空)。绝大多数 OneSIG v2.5.x 设备 nginx 已经把 `/v3/` - 直接路由到后端,`https://host/v3/captcha` 直接返回 JSON。 - - 如果设备前面挂了反向代理(路径是 `https://host/api/v3/...`),把 - `api_prefix` 显式改成 `"/api"`。 - - 看到日志 `HTTP 404` 几乎都是 `api_prefix` 没对齐:把当前值取反 - (空 ↔ "/api")再试。 - - `verify_ssl` 由表单底部「SSL 验证」开关控制,按以下优先级取值: - 1. `verify_ssl` (主键) - 2. `ssl_verify` (兼容别名) - 3. `verifySsl` (历史 camelCase 别名) - 4. `custom_settings.verify_ssl` (WebUI 表单写入位置) - 5. 环境变量 `ONESIG_V2_5_3_D20250710_VERIFY_SSL` (本插件专用) - 6. 环境变量 `ONESIG_VERIFY_SSL` (与新版插件共用兜底) - 7. 兜底默认值 `False`(**默认关闭证书验证**) - - ⚠️ 安全提示:本插件登录阶段以**明文**形式下发密码,请仅在「内网受控网络 - + HTTPS + verify_ssl=true」的部署上启用,避免凭据泄露。 - - Cookie 会话持久化(`persist_cookies`): - - 处理器会在登录成功后把 aiohttp CookieJar 序列化成 JSON 写入 - `~/.flocks/config/.secret.json`(与口令同居一份 0600 权限文件), - key 形如 `onesig_v2_5_3_D20250710_session_cookie__`。 - - 进程重启时若该条 secret 还在且至少有一个 cookie 未过期,会直接灌回 - jar 并跳过 captcha→/v3/login→/v3/account 整条链路;如果设备已经 - 吊销该 cookie,首次业务请求拿到 401 / responseCode 1019..1022 时 - 会触发自动重登一次(与原行为一致)。 - - `logout` 动作会同步删除磁盘上的 cookie;`close` 不会删(用于优雅 - 关停时保留持久化数据)。 - - 字段优先级: - 1. `persist_cookies` - 2. `persistCookies` - 3. `custom_settings.persist_cookies` - 4. 环境变量 `ONESIG_V2_5_3_D20250710_PERSIST_COOKIES` - 5. 环境变量 `ONESIG_PERSIST_COOKIES` - 6. 兜底默认值 `True`(**默认开启持久化**) - - 说明: - - 文档接口路径形如 `/v3/...`;处理器在拼接 URL 时会自动加上 `api_prefix`, - 请按部署环境调整。 - - 当设备启用图形验证码或 TOTP 时,可在 `onesig_v2_5_3_D20250710_login` 工具的 - `login` 动作中传入 `captcha` / `totp` 参数;TOTP 一次性口令本身不加密。 - - 处理器在 401 / responseCode 1019..1022 等会话失效场景自动重新登录并重试一次; - 也可以手动调用 `onesig_v2_5_3_D20250710_login` 的 `login` / `logout` 动作管理会话。 - - 敏感字段加密(与新版 onesig 完全一致,与登录链路*独立*): - - 改密、删除用户、清空审计、接口启停、设备升级等接口的 `password` 字段 - 仍由处理器自动 RSA-OAEP 加密(每次拉最新 `/v3/pubkey`,对齐前端 - `clearRSACache`)。 - - 哈希参数由 `oaep_hash` 控制,默认 `sha1`;如改密返回 `1009/1017` - 但口令正确,请尝试切换为 `sha256`: - api_services.onesig_v2_5_3_D20250710_api.oaep_hash = "sha256" - - multipart 上传: - - 多个 `导入 / 上传 / 升级` 类接口(asset_import、tls_cert_create、 - system_upgrade、device_upgrade_upload、basic_information_import、 - basic_license_upload、backup_import 等)会以 `multipart/form-data` - 提交本地文件,调用方需提供 `file_path` 指向本地绝对路径。 diff --git a/.flocks/flockshub/plugins/tools/api/onesig_v2_5_3_D20250710/_test.yaml b/.flocks/flockshub/plugins/tools/api/onesig_v2_5_3_D20250710/_test.yaml deleted file mode 100644 index 64a97abee..000000000 --- a/.flocks/flockshub/plugins/tools/api/onesig_v2_5_3_D20250710/_test.yaml +++ /dev/null @@ -1,116 +0,0 @@ -schema_version: 1 -provider: onesig_v2_5_3_D20250710_api - -# Service-level connectivity probe. -# `basic_version` is a lightweight GET that verifies the device is reachable -# and the session cookie / credentials are valid, without side-effects. -connectivity: - tool: onesig_v2_5_3_D20250710_device - params: - action: basic_version - -# Tool-level test samples shown in the WebUI ToolDetailDrawer drop-down. -# `label` is the default (English) display string; `label_cn` is the -# optional Chinese override picked when the WebUI runs in zh-CN. -fixtures: - onesig_v2_5_3_D20250710_login: - - label: "Get current account info" - label_cn: "获取当前登录账户信息" - tags: [smoke] - params: - action: get_account - assert: - success: true - - - label: "Get product news" - label_cn: "获取产品公告" - tags: [smoke] - params: - action: get_product_news - - onesig_v2_5_3_D20250710_monitoring: - - label: "List threat type options" - label_cn: "获取威胁类型枚举" - tags: [smoke] - params: - action: common_threat_type_list - assert: - success: true - - - label: "Get dashboard status" - label_cn: "获取仪表板状态" - tags: [smoke] - params: - action: dashboard_status - - - label: "Get device platform status" - label_cn: "获取设备平台状态" - tags: [smoke] - params: - action: device_platform_status - - onesig_v2_5_3_D20250710_strategy: - - label: "Get blacklist location options" - label_cn: "获取黑名单地理位置枚举" - tags: [smoke] - params: - action: blacklist_location_options - assert: - success: true - - - label: "List whitelist entries (page 1)" - label_cn: "查询全局白名单列表(第 1 页)" - tags: [smoke] - params: - action: whitelist_list - - onesig_v2_5_3_D20250710_assets: - - label: "Get asset group tree" - label_cn: "获取资产分组树" - tags: [smoke] - params: - action: common_asset_group_tree - assert: - success: true - - - label: "List assets (page 1)" - label_cn: "查询资产列表(第 1 页)" - tags: [smoke] - params: - action: asset_list - - onesig_v2_5_3_D20250710_device: - - label: "Get device version" - label_cn: "获取设备版本信息" - tags: [smoke] - params: - action: basic_version - assert: - success: true - - - label: "Get license info" - label_cn: "获取许可证信息" - tags: [smoke] - params: - action: basic_license_get - - - label: "Get device network status" - label_cn: "获取设备网络状态" - tags: [smoke] - params: - action: device_network_status - - onesig_v2_5_3_D20250710_helper: - - label: "Get product version" - label_cn: "获取产品版本" - tags: [smoke] - params: - action: product_version - assert: - success: true - - - label: "List documents (page 1)" - label_cn: "获取文档列表(第 1 页)" - tags: [smoke] - params: - action: document_list diff --git a/.flocks/flockshub/plugins/tools/api/onesig_v2_5_3_D20250710/onesig_v2_5_3_D20250710.handler.py b/.flocks/flockshub/plugins/tools/api/onesig_v2_5_3_D20250710/onesig_v2_5_3_D20250710.handler.py deleted file mode 100644 index b8c95f151..000000000 --- a/.flocks/flockshub/plugins/tools/api/onesig_v2_5_3_D20250710/onesig_v2_5_3_D20250710.handler.py +++ /dev/null @@ -1,2270 +0,0 @@ -from __future__ import annotations - -import asyncio -import base64 -import hashlib -import json -import os -import ssl -import time -from email.utils import parsedate_to_datetime -from http.cookies import Morsel, SimpleCookie -from typing import Any, Optional - -import aiohttp -from yarl import URL - -from flocks.config.config_writer import ConfigWriter -from flocks.tool.registry import ToolContext, ToolResult - - -SERVICE_ID = "onesig_v2_5_3_D20250710_api" - -# OneSIG v2.5.x 老版本(``onesig_v2_5_3_D20250710``)设备直接监听 ``/v3/...``,没有 -# 任何额外前缀;与新版插件对齐,``api_prefix`` 留空即可。需要前缀的部署在 UI -# 上把 ``api_prefix`` 设成 ``"/api"`` 等值即可覆盖。 -DEFAULT_API_PREFIX = "" -# OAEP 哈希仅用于*非登录*的敏感字段加密(改密、删用户、删审计日志等), -# 登录阶段已切换为明文密码。保留该配置项是为了与新版 onesig 共享同一份 -# encrypt_fields 写操作链路。 -DEFAULT_OAEP_HASH = "sha1" -DEFAULT_TIMEOUT = 60 -DEFAULT_VERIFY_SSL = False -DEFAULT_PERSIST_COOKIES = True - -# Bumped whenever the on-disk shape under -# ``onesig_v2_5_3_D20250710_session_cookie__*`` changes in an incompatible way; -# older snapshots are silently discarded. Prefix is intentionally distinct -# from the encrypted-login plugin so the two coexist without clobbering -# each other's persisted cookie jars. -_COOKIE_SNAPSHOT_VERSION = 1 -_COOKIE_SECRET_PREFIX = "onesig_v2_5_3_D20250710_session_cookie__" - -_RESPONSE_CODE_OK = 0 -_RESPONSE_CODE_TOTP_REQUIRED = 1012 -_RESPONSE_CODE_DEFAULT_PWD = 1010 -_RESPONSE_CODE_PWD_EXPIRED = 1011 - -_SESSION_EXPIRED_RESPONSE_CODES = frozenset({1019, 1020, 1021, 1022}) -_SESSION_EXPIRED_HTTP_STATUSES = frozenset({401, 403}) - - -def _get_secret_manager() -> Any: - from flocks.security import get_secret_manager - - return get_secret_manager() - - -def _resolve_ref(value: Any) -> Optional[str]: - if value is None: - return None - if not isinstance(value, str): - return str(value) - if value.startswith("{secret:") and value.endswith("}"): - return _get_secret_manager().get(value[len("{secret:") : -1]) - if value.startswith("{env:") and value.endswith("}"): - return os.getenv(value[len("{env:") : -1]) - return value - - -def _service_config() -> dict[str, Any]: - raw = ConfigWriter.get_api_service_raw(SERVICE_ID) - return raw if isinstance(raw, dict) else {} - - -def _coerce_bool(value: Any, default: bool = True) -> bool: - if isinstance(value, bool): - return value - if value is None: - return default - if isinstance(value, (int, float)): - return bool(value) - text = str(value).strip().lower() - if text in {"1", "true", "yes", "y", "on"}: - return True - if text in {"0", "false", "no", "n", "off"}: - return False - return default - - -def _resolve_verify_ssl(raw: dict[str, Any]) -> bool: - """Resolve the SSL verification toggle from a service-config dict. - - Mirrors the PR #193 cross-handler convention so that the WebUI's - "SSL verify" switch (which writes to ``custom_settings.verify_ssl``) - drives onesec / ngtip / qingteng / onesig **uniformly**. Lookup order: - - 1. ``raw["verify_ssl"]`` - canonical - 2. ``raw["ssl_verify"]`` - snake_case alias (PR #193) - 3. ``raw["verifySsl"]`` - camelCase alias (onesig legacy) - 4. ``raw["custom_settings"]["verify_ssl"]`` - WebUI generic toggle - 5. ``ONESIG_VERIFY_SSL`` env var - onesig-specific override - 6. fallback to ``DEFAULT_VERIFY_SSL`` - default ``False`` (parity - with onesec / ngtip / qingteng - after PR #193): OneSIG is - almost always deployed as a - private gateway with self- - signed certs, so the open- - box behaviour is to skip - validation. Flip the toggle - on to enforce certificate - checks for public / signed - deployments. - - String values are normalised through ``_coerce_bool`` so that any of - ``"true"/"false"/"1"/"0"/"yes"/"no"/"on"/"off"`` work consistently with - the rest of the codebase. - """ - candidates: list[Any] = [ - raw.get("verify_ssl"), - raw.get("ssl_verify"), - raw.get("verifySsl"), - ] - custom = raw.get("custom_settings") - if isinstance(custom, dict): - candidates.append(custom.get("verify_ssl")) - # Prefer the plaintext-login-specific env var, fall back to the shared - # ``ONESIG_VERIFY_SSL`` so a single CLI/container env can drive both - # plugins side-by-side without re-declaring the toggle twice. - candidates.append(os.getenv("ONESIG_V2_5_3_D20250710_VERIFY_SSL")) - candidates.append(os.getenv("ONESIG_VERIFY_SSL")) - - for value in candidates: - if value is None: - continue - return _coerce_bool(value, default=DEFAULT_VERIFY_SSL) - return DEFAULT_VERIFY_SSL - - -def _resolve_persist_cookies(raw: dict[str, Any]) -> bool: - """Resolve the cookie-persistence toggle. - - OneSIG sessions are cookie-based; persisting the jar to ``.secret.json`` - lets a flocks restart skip the captcha → pubkey → /v3/login → /v3/account - chain (~4 RTT) and reuse the still-valid cookie until the device returns - 401 / responseCode 1019..1022, at which point the existing auto-relogin - path takes over. - - Same shape as :func:`_resolve_verify_ssl`: - - 1. ``raw["persist_cookies"]`` - canonical - 2. ``raw["persistCookies"]`` - camelCase alias - 3. ``raw["custom_settings"]["persist_cookies"]`` - WebUI generic toggle - 4. ``ONESIG_PERSIST_COOKIES`` env var - CLI / container - 5. fallback to ``DEFAULT_PERSIST_COOKIES`` - ``True`` - """ - candidates: list[Any] = [ - raw.get("persist_cookies"), - raw.get("persistCookies"), - ] - custom = raw.get("custom_settings") - if isinstance(custom, dict): - candidates.append(custom.get("persist_cookies")) - candidates.append(os.getenv("ONESIG_V2_5_3_D20250710_PERSIST_COOKIES")) - candidates.append(os.getenv("ONESIG_PERSIST_COOKIES")) - - for value in candidates: - if value is None: - continue - return _coerce_bool(value, default=DEFAULT_PERSIST_COOKIES) - return DEFAULT_PERSIST_COOKIES - - -class OneSIGRuntimeConfig: - """Resolved runtime configuration for a single OneSIG service entry.""" - - def __init__( - self, - *, - base_url: str, - api_prefix: str, - username: str, - password: str, - oaep_hash: str, - verify_ssl: bool, - timeout: int, - persist_cookies: bool = DEFAULT_PERSIST_COOKIES, - ) -> None: - self.base_url = base_url - self.api_prefix = api_prefix - self.username = username - self.password = password - self.oaep_hash = oaep_hash - self.verify_ssl = verify_ssl - self.timeout = timeout - self.persist_cookies = persist_cookies - - @property - def session_key(self) -> str: - return f"{self.base_url}|{self.username}" - - def build_url(self, path: str) -> str: - path = path if path.startswith("/") else "/" + path - prefix = self.api_prefix.rstrip("/") - return f"{self.base_url}{prefix}{path}" - - -def _resolve_runtime_config() -> OneSIGRuntimeConfig: - raw = _service_config() - base_url = ( - _resolve_ref(raw.get("base_url")) - or _resolve_ref(raw.get("baseUrl")) - or os.getenv("ONESIG_V2_5_3_D20250710_BASE_URL") - or os.getenv("ONESIG_BASE_URL") - ) - if not base_url: - raise ValueError( - "OneSIG (older v2.5) base_url not configured. Set " - "api_services.onesig_v2_5_3_D20250710_api.base_url or " - "ONESIG_V2_5_3_D20250710_BASE_URL." - ) - base_url = base_url.rstrip("/") - - api_prefix = ( - _resolve_ref(raw.get("api_prefix")) - or _resolve_ref(raw.get("apiPrefix")) - or os.getenv("ONESIG_V2_5_3_D20250710_API_PREFIX") - or os.getenv("ONESIG_API_PREFIX") - or DEFAULT_API_PREFIX - ) - if api_prefix and not api_prefix.startswith("/"): - api_prefix = "/" + api_prefix - api_prefix = api_prefix.rstrip("/") - - username = ( - _resolve_ref(raw.get("username")) - or _resolve_ref(raw.get("user")) - or os.getenv("ONESIG_V2_5_3_D20250710_USERNAME") - or os.getenv("ONESIG_USERNAME") - ) - if not username: - raise ValueError( - "OneSIG (older v2.5) username not configured. Set " - "api_services.onesig_v2_5_3_D20250710_api.username or " - "ONESIG_V2_5_3_D20250710_USERNAME." - ) - - secret_manager = _get_secret_manager() - password = ( - _resolve_ref(raw.get("password")) - or secret_manager.get("onesig_v2_5_3_D20250710_password") - or secret_manager.get(f"{SERVICE_ID}_password") - or os.getenv("ONESIG_V2_5_3_D20250710_PASSWORD") - or os.getenv("ONESIG_PASSWORD") - ) - if not password: - raise ValueError( - "OneSIG (older v2.5) password not configured. Save it as the " - "onesig_v2_5_3_D20250710_password secret or set ONESIG_V2_5_3_D20250710_PASSWORD." - ) - - # OAEP hash 只对 *非登录* 的敏感字段(改密 / 删用户 / 删审计 / 接口启停 / - # 升级口令)有效;登录阶段已改为明文,详见 ``OneSIGSession.login``。 - oaep_hash = ( - _resolve_ref(raw.get("oaep_hash")) - or _resolve_ref(raw.get("oaepHash")) - or os.getenv("ONESIG_V2_5_3_D20250710_OAEP_HASH") - or os.getenv("ONESIG_OAEP_HASH") - or DEFAULT_OAEP_HASH - ).lower() - if oaep_hash not in {"sha1", "sha256"}: - oaep_hash = DEFAULT_OAEP_HASH - - verify_ssl = _resolve_verify_ssl(raw) - persist_cookies = _resolve_persist_cookies(raw) - - timeout_raw = raw.get("timeout", DEFAULT_TIMEOUT) - try: - timeout = int(timeout_raw) - except (TypeError, ValueError): - timeout = DEFAULT_TIMEOUT - - return OneSIGRuntimeConfig( - base_url=base_url, - api_prefix=api_prefix, - username=username, - password=password, - oaep_hash=oaep_hash, - verify_ssl=verify_ssl, - timeout=timeout, - persist_cookies=persist_cookies, - ) - - -def _rsa_oaep_encrypt(pem_pubkey: str, plain: str, oaep_hash: str) -> str: - """Encrypt `plain` with RSA-OAEP using the provided PEM public key.""" - from cryptography.hazmat.backends import default_backend - from cryptography.hazmat.primitives import hashes, serialization - from cryptography.hazmat.primitives.asymmetric import padding - - hash_alg = hashes.SHA1() if oaep_hash == "sha1" else hashes.SHA256() - pub = serialization.load_pem_public_key( - pem_pubkey.encode("utf-8"), backend=default_backend() - ) - cipher = pub.encrypt( - plain.encode("utf-8"), - padding.OAEP( - mgf=padding.MGF1(algorithm=hash_alg), - algorithm=hash_alg, - label=None, - ), - ) - return base64.b64encode(cipher).decode("ascii") - - -def _ssl_context(verify_ssl: bool) -> Any: - if verify_ssl: - return None - ctx = ssl.create_default_context() - ctx.check_hostname = False - ctx.verify_mode = ssl.CERT_NONE - return ctx - - -# --------------------------------------------------------------------------- -# Cookie persistence (.secret.json round-trip) -# --------------------------------------------------------------------------- -# OneSIG sessions are cookie-based and the device hands out a session cookie -# only after the captcha → pubkey → /v3/login dance. To avoid re-running that -# 4-RTT dance after every flocks restart we serialise the cookie jar to the -# existing ``~/.flocks/config/.secret.json`` (mode 0600) under a per-device -# secret_id, then re-hydrate it when a fresh ``OneSIGSession`` is constructed. -# -# Format choices intentionally avoid ``aiohttp.CookieJar.save/load`` (pickle): -# - JSON keeps the file human-readable for ops debugging. -# - JSON is immune to deserialisation-as-RCE if .secret.json ever leaks -# write privileges. -# - The shape is versioned so future changes can simply bump -# ``_COOKIE_SNAPSHOT_VERSION`` and discard older snapshots. - - -def _cookie_secret_id(base_url: str, username: str) -> str: - """Stable, filesystem-/JSON-safe secret id for a (device, account) pair. - - The pair is hashed (sha1, truncated) so URL/IP/port special chars never - leak into the secret_id namespace, and so the same secret slot is reused - across reconfigurations of the same logical session. - """ - digest = hashlib.sha1( - f"{base_url}|{username}".encode("utf-8") - ).hexdigest()[:12] - return f"{_COOKIE_SECRET_PREFIX}{digest}" - - -def _cookies_to_snapshot(jar: aiohttp.CookieJar) -> list[dict[str, Any]]: - """Snapshot every Morsel in ``jar`` into a list of plain JSON-able dicts.""" - rows: list[dict[str, Any]] = [] - for morsel in jar: # iterates over http.cookies.Morsel - rows.append( - { - "name": morsel.key, - "value": morsel.value, - "domain": morsel["domain"] or "", - "path": morsel["path"] or "/", - "expires": morsel["expires"] or "", - "secure": bool(morsel["secure"]), - "httponly": bool(morsel["httponly"]), - } - ) - return rows - - -def _is_cookie_expired(expires: str, *, now: Optional[float] = None) -> bool: - """Best-effort check on RFC 1123 ``Expires`` string. Unparseable → False - (defer to aiohttp's own jar logic; we don't want to silently drop cookies - just because the device returned a non-standard expires format).""" - if not expires: - return False - try: - exp_dt = parsedate_to_datetime(expires) - except (TypeError, ValueError): - return False - if exp_dt is None: - return False - ts = exp_dt.timestamp() - return ts <= (now if now is not None else time.time()) - - -def _snapshot_into_jar( - jar: aiohttp.CookieJar, - rows: list[dict[str, Any]], - base_url: str, -) -> int: - """Inflate ``rows`` (output of :func:`_cookies_to_snapshot`) back into - an aiohttp jar. Already-expired cookies are silently dropped. Returns - the number of cookies actually injected.""" - if not rows: - return 0 - sc: SimpleCookie = SimpleCookie() - injected = 0 - for row in rows: - name = row.get("name") or "" - if not name: - continue - if _is_cookie_expired(row.get("expires") or ""): - continue - value = row.get("value") or "" - sc[name] = value - m: Morsel = sc[name] - if row.get("domain"): - m["domain"] = row["domain"] - if row.get("path"): - m["path"] = row["path"] - if row.get("expires"): - m["expires"] = row["expires"] - if row.get("secure"): - m["secure"] = True - if row.get("httponly"): - m["httponly"] = True - injected += 1 - if injected == 0: - return 0 - # response_url seeds the jar's domain/path bookkeeping for any cookie - # that didn't carry an explicit Domain attribute (typical for OneSIG, - # which scopes cookies to the device host). - try: - response_url = URL(base_url) - except Exception: - response_url = URL("http://localhost") - jar.update_cookies(sc, response_url=response_url) - return injected - - -def _load_cookie_snapshot(secret_id: str) -> Optional[dict[str, Any]]: - """Pull a previously persisted snapshot dict, dropping malformed or - fully-expired payloads. Returns ``None`` when there is nothing usable. - - Failure modes (corrupt JSON, version mismatch, all cookies expired) are - swallowed so a poisoned secret never breaks the calling tool — the - handler will simply fall through to a fresh login.""" - raw = _get_secret_manager().get(secret_id) - if not raw: - return None - try: - data = json.loads(raw) - except (json.JSONDecodeError, TypeError): - return None - if not isinstance(data, dict): - return None - if data.get("version") != _COOKIE_SNAPSHOT_VERSION: - return None - cookies = data.get("cookies") - if not isinstance(cookies, list): - return None - fresh = [ - c for c in cookies - if isinstance(c, dict) - and c.get("name") - and not _is_cookie_expired(c.get("expires") or "") - ] - if not fresh: - return None - data["cookies"] = fresh - return data - - -def _save_cookie_snapshot(secret_id: str, snapshot: dict[str, Any]) -> bool: - """Persist a snapshot. Best-effort: returns False on I/O failure but - never raises — cookie persistence is an optimisation, not a hard - requirement of the request path.""" - try: - _get_secret_manager().set( - secret_id, json.dumps(snapshot, separators=(",", ":")) - ) - return True - except Exception: - return False - - -def _delete_cookie_snapshot(secret_id: str) -> bool: - try: - return _get_secret_manager().delete(secret_id) - except Exception: - return False - - -class OneSIGSession: - """Cookie-based session for a OneSIG device. - - A single instance owns an aiohttp ClientSession with its own cookie jar and - handles the captcha → pubkey → login flow. Concurrent callers reuse the - same logged-in session and share the auto-relogin lock. - """ - - def __init__(self, config: OneSIGRuntimeConfig) -> None: - self.config = config - self._session: Optional[aiohttp.ClientSession] = None - self._logged_in = False - self._login_lock = asyncio.Lock() - # Cookies waiting to be injected into the jar at the next - # ``_ensure_session`` call. Populated synchronously from - # ``.secret.json`` here so that tests / dispatchers can observe - # ``_logged_in`` immediately after construction. - self._pending_cookies: Optional[list[dict[str, Any]]] = None - self._cookies_loaded = False - - if self.config.persist_cookies: - snapshot = _load_cookie_snapshot(self._cookie_secret_id) - cookies = (snapshot or {}).get("cookies") if snapshot else None - if cookies: - self._pending_cookies = cookies - # Trust the persisted cookie. If the device has rotated / - # invalidated it the existing 401-or-1019..1022 auto-relogin - # path in ``request()`` will repair the session on first - # business call. No extra RTT lost vs. unconditional login. - self._logged_in = True - - @property - def _cookie_secret_id(self) -> str: - return _cookie_secret_id(self.config.base_url, self.config.username) - - async def _ensure_session(self) -> aiohttp.ClientSession: - if self._session is None or self._session.closed: - jar = aiohttp.CookieJar(unsafe=True) - if self._pending_cookies and not self._cookies_loaded: - try: - _snapshot_into_jar( - jar, self._pending_cookies, self.config.base_url - ) - except Exception: - # Bad on-disk snapshot must not block the request path: - # forget the persisted cookies and force a clean login. - self._logged_in = False - self._cookies_loaded = True - self._pending_cookies = None - self._session = aiohttp.ClientSession(cookie_jar=jar) - return self._session - - def _persist_cookies(self) -> None: - """Snapshot the current jar to ``.secret.json``. Called after every - successful login / re-login. No-op when persistence is disabled or - the jar is empty. - - Best-effort: any exception (missing ``cookie_jar`` on a swapped-in - test double, FS error, JSON encode bug, …) is swallowed so the - request path is never blocked by a persistence side-effect.""" - if not self.config.persist_cookies: - return - if self._session is None or self._session.closed: - return - try: - jar = getattr(self._session, "cookie_jar", None) - if jar is None: - return - rows = _cookies_to_snapshot(jar) - if not rows: - return - snapshot = { - "version": _COOKIE_SNAPSHOT_VERSION, - "session_key": self.config.session_key, - "saved_at": int(time.time()), - "cookies": rows, - } - _save_cookie_snapshot(self._cookie_secret_id, snapshot) - except Exception: - return - - def _drop_persisted_cookies(self) -> None: - if self.config.persist_cookies: - _delete_cookie_snapshot(self._cookie_secret_id) - - async def close(self) -> None: - # ``close`` is for graceful shutdown (e.g. process exit). It does NOT - # drop the persisted cookie — that's the whole point of persistence. - # Use ``logout()`` for an explicit invalidation instead. - if self._session is not None and not self._session.closed: - await self._session.close() - self._session = None - self._logged_in = False - - async def login( - self, - *, - captcha: Optional[str] = None, - totp: Optional[str] = None, - force: bool = False, - ) -> dict[str, Any]: - """Run the *plaintext-password* login flow for OneSIG v2.5 older devices. - - Sequence: ``GET /v3/captcha`` → ``POST /v3/login`` (with plaintext - ``password``) → optional ``POST /v3/login/totp``. The - ``GET /v3/pubkey`` + RSA-OAEP encryption step from the standard - ``onesig`` plugin is intentionally omitted here — older firmware - rejects RSA ciphertext on the login endpoint and only accepts the - raw password. - - Returns the final account info on success. Raises ValueError on - failure with a human-readable explanation. - """ - async with self._login_lock: - if self._logged_in and not force: - account = await self._raw_request_json("GET", "/v3/account") - if isinstance(account, dict) and account.get("responseCode") == _RESPONSE_CODE_OK: - return account.get("data", {}) or {} - self._logged_in = False - - captcha_info = await self._raw_request_json("GET", "/v3/captcha") - captcha_data = ( - captcha_info.get("data", {}) if isinstance(captcha_info, dict) else {} - ) - enable_captcha = bool(captcha_data.get("enableCaptcha")) - enable_totp_inline = bool(captcha_data.get("enableTotp")) - if enable_captcha and not captcha: - raise ValueError( - "OneSIG 设备已启用图形验证码,请通过 onesig_v2_5_3_D20250710_login(action='login', captcha='...') 提供。" - ) - if enable_totp_inline and not totp: - # Inline mode: the captcha endpoint already advertises - # `enableTotp=true`, meaning the device wants the TOTP code on - # the same login form (the `checksum` field) rather than the - # post-login QR-scan flow. Refuse early so the caller does not - # see an opaque 1017 / "checksum 不能为空" later. - raise ValueError( - "OneSIG 设备已启用 inline TOTP(同屏「用户口令」),请通过 " - "onesig_v2_5_3_D20250710_login(action='login', totp='...') 提供动态口令或恢复码。" - ) - - # --------------------------------------------------------- - # OneSIG v2.5 老版本走 *明文* 密码登录链路 —— - # - # 与新版 ``onesig`` 插件最显著的区别:跳过 ``GET /v3/pubkey`` + - # RSA-OAEP 加密这一对步骤,直接把 ``self.config.password`` - # 的明文塞进 ``payload["password"]`` 发给 ``POST /v3/login``。 - # - # 设计取舍: - # * 老版本设备早期部署(v2.5 之前的中间发布或定制版)只接受 - # 明文密码登录,下发 RSA 密文反而会被解析成乱码导致 - # ``responseCode=1009`` / ``1017``。 - # * 业务路径上的*敏感写字段*(改密、删用户、删审计、接口启停、 - # 设备升级口令等)仍走 ``encrypt_with_pubkey`` / - # ``_encrypt_body_fields``,复用同一份 OAEP 实现。换言之: - # **只有登录阶段是明文**,写操作字段加密保持与新版一致。 - # * 该插件应仅用于内网受控网络中的老固件,避免明文凭证暴露 - # 于未加密信道;HTTPS + 启用 ``verify_ssl`` 是强烈建议项。 - # --------------------------------------------------------- - payload: dict[str, Any] = { - "username": self.config.username, - "password": self.config.password, - } - if enable_captcha and captcha: - payload["captcha"] = captcha - if enable_totp_inline and totp: - payload["checksum"] = totp - - login_resp = await self._raw_request_json( - "POST", "/v3/login", json_body=payload - ) - if not isinstance(login_resp, dict): - raise ValueError(f"登录返回非 JSON:{login_resp!r}") - - response_code = login_resp.get("responseCode") - if response_code == _RESPONSE_CODE_TOTP_REQUIRED: - if not totp: - raise ValueError( - "OneSIG 设备要求扫码 TOTP 二次验证,请在 login 调用中传入 `totp` 参数。" - ) - totp_resp = await self._raw_request_json( - "POST", - "/v3/login/totp", - json_body={"checksum": totp}, - ) - if ( - not isinstance(totp_resp, dict) - or totp_resp.get("responseCode") != _RESPONSE_CODE_OK - ): - raise ValueError( - f"TOTP 二次验证失败:{(totp_resp or {}).get('verboseMsg', totp_resp)}" - ) - elif response_code in (_RESPONSE_CODE_DEFAULT_PWD, _RESPONSE_CODE_PWD_EXPIRED): - raise ValueError( - f"OneSIG 要求修改密码(responseCode={response_code}:{login_resp.get('verboseMsg')})。" - " 请先通过控制台或 `onesig_v2_5_3_D20250710_login(action='change_password', ...)` 修改密码。" - ) - elif response_code != _RESPONSE_CODE_OK: - raise ValueError( - f"OneSIG 登录失败(responseCode={response_code}):{login_resp.get('verboseMsg')}" - ) - - self._logged_in = True - # Persist immediately on successful login (before /v3/account so - # cookie survives even if the verification call fails). Best- - # effort: failures here never break the calling tool. - self._persist_cookies() - account_resp = await self._raw_request_json("GET", "/v3/account") - if ( - isinstance(account_resp, dict) - and account_resp.get("responseCode") == _RESPONSE_CODE_OK - ): - return account_resp.get("data", {}) or {} - return {} - - async def logout(self) -> dict[str, Any]: - try: - resp = await self._raw_request_json("POST", "/v3/logout", json_body={}) - finally: - self._logged_in = False - # Logout invalidates the cookie server-side; remove the local - # snapshot too so the next process doesn't try to reuse a dead - # session and pay an extra round-trip discovering it. - self._drop_persisted_cookies() - if isinstance(resp, dict): - return resp - return {} - - async def _raw_request_json( - self, - method: str, - path: str, - *, - params: Optional[dict[str, Any]] = None, - json_body: Optional[Any] = None, - ) -> Any: - """Issue a raw request and parse the JSON envelope. Used for login/logout/account.""" - session = await self._ensure_session() - url = self.config.build_url(path) - request_params = {"lang": "zh"} - if params: - request_params.update({k: v for k, v in params.items() if v is not None}) - kwargs: dict[str, Any] = { - "params": request_params, - "timeout": aiohttp.ClientTimeout(total=self.config.timeout), - "ssl": _ssl_context(self.config.verify_ssl), - } - if json_body is not None: - kwargs["json"] = json_body - kwargs["headers"] = {"Content-Type": "application/json"} - async with session.request(method.upper(), url, **kwargs) as resp: - text = await resp.text() - try: - parsed = await resp.json(content_type=None) - except Exception: - parsed = None - # ``aiohttp.ClientResponse.json(content_type=None)`` returns - # ``None`` for empty / whitespace-only bodies *without raising*, - # which used to surface as the very confusing - # ``无法从 /v3/pubkey 获取 RSA 公钥:None`` - # 在错误日志里 — 看不到 status / URL,根本没法定位到「``/api`` 前 - # 缀错了导致 404」之类的根因。这里把 ``None`` 也统一塞进 fallback - # 字典,让上层的报错带上 status 和 body 摘要。 - if parsed is None or not isinstance(parsed, (dict, list)): - return { - "_status": resp.status, - "_url": str(resp.url), - "_text": text[:500], - } - return parsed - - async def encrypt_with_pubkey(self, plain: str) -> str: - """Fetch the latest /v3/pubkey and RSA-OAEP encrypt the given plaintext. - - Mirrors the front-end "clearRSACache → fresh pubkey → encrypt" pattern - (`@/util/rsa.js`) used for sensitive write operations. - """ - pubkey_resp = await self._raw_request_json("GET", "/v3/pubkey") - pubkey = (pubkey_resp or {}).get("data", {}).get("pubkey") - if not pubkey: - raise ValueError( - f"无法从 /v3/pubkey 获取 RSA 公钥用于字段加密:{pubkey_resp!r}" - ) - return _rsa_oaep_encrypt(pubkey, plain, self.config.oaep_hash) - - async def request( - self, - method: str, - path: str, - *, - params: Optional[dict[str, Any]] = None, - json_body: Optional[Any] = None, - form_data: Optional["aiohttp.FormData"] = None, - captcha: Optional[str] = None, - totp: Optional[str] = None, - _retry: bool = True, - ) -> tuple[int, dict[str, Any], bytes, str]: - """Issue an authenticated request, auto-relogin on session expiry. - - Pass either ``json_body`` (JSON request) or ``form_data`` - (``multipart/form-data`` upload). Passing both raises ValueError. - - Returns ``(status, json_envelope, body_bytes, content_type)``. When the - response is JSON, ``json_envelope`` contains the parsed payload and - ``body_bytes`` is empty. When the response is binary, ``body_bytes`` - carries the raw bytes for the caller to persist. - """ - if json_body is not None and form_data is not None: - raise ValueError("request() cannot accept both json_body and form_data") - - if not self._logged_in: - await self.login(captcha=captcha, totp=totp) - - session = await self._ensure_session() - url = self.config.build_url(path) - request_params: dict[str, Any] = {"lang": "zh"} - if params: - request_params.update({k: v for k, v in params.items() if v is not None}) - - kwargs: dict[str, Any] = { - "params": request_params, - "timeout": aiohttp.ClientTimeout(total=self.config.timeout), - "ssl": _ssl_context(self.config.verify_ssl), - } - if json_body is not None: - kwargs["json"] = json_body - kwargs["headers"] = {"Content-Type": "application/json"} - elif form_data is not None: - # aiohttp sets Content-Type with the boundary for FormData - # automatically; do not override. - kwargs["data"] = form_data - - async with session.request(method.upper(), url, **kwargs) as resp: - status = resp.status - content_type = resp.headers.get("Content-Type", "") or "" - if "application/json" in content_type: - envelope = await resp.json(content_type=None) - body_bytes = b"" - else: - body_bytes = await resp.read() - envelope = {} - - envelope_dict = envelope if isinstance(envelope, dict) else {"data": envelope} - response_code = envelope_dict.get("responseCode") if envelope_dict else None - session_expired = ( - status in _SESSION_EXPIRED_HTTP_STATUSES - or response_code in _SESSION_EXPIRED_RESPONSE_CODES - ) - if session_expired and _retry: - self._logged_in = False - await self.login(captcha=captcha, totp=totp, force=True) - # form_data is single-shot; caller must rebuild on retry. We can - # only retransmit JSON requests automatically. - if form_data is not None: - return status, envelope_dict, body_bytes, content_type - return await self.request( - method, - path, - params=params, - json_body=json_body, - captcha=captcha, - totp=totp, - _retry=False, - ) - return status, envelope_dict, body_bytes, content_type - - -_SESSIONS: dict[str, OneSIGSession] = {} -_SESSIONS_LOCK = asyncio.Lock() - - -async def _get_session(config: OneSIGRuntimeConfig) -> OneSIGSession: - async with _SESSIONS_LOCK: - sess = _SESSIONS.get(config.session_key) - if sess is None: - sess = OneSIGSession(config) - _SESSIONS[config.session_key] = sess - else: - sess.config = config - return sess - - -# --------------------------------------------------------------------------- -# Action specifications -# --------------------------------------------------------------------------- - - -_RESERVED_PARAM_KEYS = frozenset({"action", "captcha", "totp", "file_path"}) - - -class ActionSpec: - """Declarative spec for a single OneSIG endpoint action.""" - - def __init__( - self, - method: str, - path: str, - *, - body_keys: Optional[list[str]] = None, - query_keys: Optional[list[str]] = None, - passthrough_body: bool = False, - binary: bool = False, - required: Optional[list[str]] = None, - multipart: bool = False, - multipart_file_field: str = "file", - encrypt_fields: Optional[tuple[str, ...]] = None, - ) -> None: - self.method = method.upper() - self.path = path - self.body_keys = body_keys or [] - self.query_keys = query_keys or [] - self.passthrough_body = passthrough_body - self.binary = binary - self.required = required or [] - # multipart=True: send the body as multipart/form-data; the local file - # path comes from the reserved `file_path` param and is uploaded under - # `multipart_file_field` (default `file`, per OneSIG docs). - self.multipart = multipart - self.multipart_file_field = multipart_file_field - # Body fields whose plaintext value must be RSA-OAEP encrypted with the - # current /v3/pubkey before being sent (typical: ("password",) or - # ("password", "dupPassword")). - self.encrypt_fields = tuple(encrypt_fields or ()) - - def build_request( - self, params: dict[str, Any] - ) -> tuple[Optional[dict[str, Any]], Optional[Any]]: - query = {k: params[k] for k in self.query_keys if params.get(k) is not None} - - body: Optional[Any] = None - if self.method == "GET" and not self.multipart: - # OneSIG 的 GET list 接口对 query 里的分页/过滤参数要求很严 - # (e.g. /v3/apikey/list 不带 pageNo/pageSize 直接回 1004)。 - # body_keys 里声明过的字段先入 query;剩余非保留、非已声明的字段 - # 也透传进 query,避免调用方传了 pageNo/severity 这类常见过滤 - # 项被 handler 静默丢掉。 - for key in self.body_keys: - if params.get(key) is not None: - query[key] = params[key] - for k, v in params.items(): - if v is None: - continue - if k in _RESERVED_PARAM_KEYS: - continue - if k in self.query_keys or k in self.body_keys: - continue - query[k] = v - else: - if self.passthrough_body: - body = { - k: v - for k, v in params.items() - if k not in _RESERVED_PARAM_KEYS - and k not in self.query_keys - and v is not None - } - else: - body = {k: params[k] for k in self.body_keys if params.get(k) is not None} - return (query or None), body - - -def _has_value(value: Any) -> bool: - if value is None: - return False - if isinstance(value, str): - return value.strip() != "" - if isinstance(value, (list, tuple, set, dict)): - return len(value) > 0 - return True - - -def _validate_required(spec: ActionSpec, action: str, params: dict[str, Any]) -> Optional[str]: - missing = [k for k in spec.required if not _has_value(params.get(k))] - if missing: - return f"Missing required parameters for {action}: {', '.join(missing)}" - return None - - -# Login / Account / Password ------------------------------------------------ - -LOGIN_ACTION_SPECS: dict[str, ActionSpec] = { - "get_captcha": ActionSpec("GET", "/v3/captcha"), - "get_pubkey": ActionSpec("GET", "/v3/pubkey"), - "get_account": ActionSpec("GET", "/v3/account"), - "get_recovery_code": ActionSpec("GET", "/v3/user/recoveryCode"), - "regenerate_recovery_code": ActionSpec("PUT", "/v3/user/recoveryCode"), - "get_product_news": ActionSpec("GET", "/v3/product/news"), - "mark_product_news_read": ActionSpec( - "PUT", "/v3/product/news", passthrough_body=True - ), -} - -# Monitoring --------------------------------------------------------------- - -MONITORING_ACTION_SPECS: dict[str, ActionSpec] = { - # dashboard - "dashboard_overview": ActionSpec("POST", "/v3/dashboard/overview", passthrough_body=True), - "dashboard_outbound": ActionSpec("POST", "/v3/dashboard/outbound", passthrough_body=True), - "dashboard_inbound": ActionSpec("POST", "/v3/dashboard/inbound", passthrough_body=True), - "dashboard_zeroday": ActionSpec("POST", "/v3/dashboard/zeroday", passthrough_body=True), - "dashboard_status": ActionSpec("GET", "/v3/dashboard/status"), - "dashboard_ioc_type_sum": ActionSpec("GET", "/v3/dashboard/ioctypesum"), - "set_custom_config": ActionSpec("PUT", "/v3/setting/customConfig", passthrough_body=True), - # overview - "common_threat_type_list": ActionSpec("GET", "/v3/common/threatTypeList"), - "overview_event_inbound": ActionSpec("POST", "/v3/overview/eventInbound", passthrough_body=True), - "overview_event_outbound": ActionSpec("POST", "/v3/overview/eventOutbound", passthrough_body=True), - "overview_export_event_inbound": ActionSpec( - "POST", "/v3/overview/exportEventInbound", passthrough_body=True, binary=True - ), - "overview_export_event_outbound": ActionSpec( - "POST", "/v3/overview/exportEventOutbound", passthrough_body=True, binary=True - ), - # OneSIG v2.5.3 实测:文档标 `incIntervalSec` 选填,但服务端实际必填, - # 不带直接回 1004 "请求数据非法"。前端总是从页面 ref 取轮询间隔传入。 - "overview_asset_brief": ActionSpec( - "POST", - "/v3/overview/assetBrief", - passthrough_body=True, - required=["startTime", "endTime", "incIntervalSec"], - ), - "overview_asset_top": ActionSpec("POST", "/v3/overview/assetTop", passthrough_body=True), - # OneSIG v2.5.3 实测:除文档明示的 startTime/endTime 外,`type` 与 - # `pageNo`/`pageSize` 也是必填,不带任何一个均回 1004。 - "overview_event_inbound_agg": ActionSpec( - "POST", - "/v3/overview/eventInboundAgg", - passthrough_body=True, - required=["startTime", "endTime", "type", "pageNo", "pageSize"], - ), - "overview_export_event_inbound_agg": ActionSpec( - "POST", "/v3/overview/exportEventInboundAgg", passthrough_body=True, binary=True - ), - "overview_event_outbound_agg": ActionSpec("POST", "/v3/overview/eventOutboundAgg", passthrough_body=True), - "overview_export_event_outbound_agg": ActionSpec( - "POST", "/v3/overview/exportEventOutboundAgg", passthrough_body=True, binary=True - ), - "overview_event_recent_agg": ActionSpec("POST", "/v3/overview/eventRecentAgg", passthrough_body=True), - # OneSIG v2.5.3 实测:`interval` 文档标选填,实际必填("1 DAY" / "1 HOUR"), - # 否则回 1004。前端按时间窗自动派发 "1 DAY" 或 "1 HOUR"。 - "overview_event_trend": ActionSpec( - "POST", - "/v3/overview/eventTrend", - passthrough_body=True, - required=["startTime", "endTime", "interval"], - ), - "overview_traffic_trend": ActionSpec("POST", "/v3/overview/trafficTrend", passthrough_body=True), - # OneSIG v2.5.3 实测:`incIntervalSec` 文档标选填,实际必填。 - "overview_stat": ActionSpec( - "POST", - "/v3/overview/stat", - passthrough_body=True, - required=["startTime", "endTime", "incIntervalSec"], - ), - "overview_threat_type_proportion": ActionSpec( - "POST", "/v3/overview/threatTypeProportion", passthrough_body=True - ), - "overview_ioc_type_proportion": ActionSpec( - "POST", "/v3/overview/iocTypeProportion", passthrough_body=True - ), - "overview_export_threat_type_proportion": ActionSpec( - "POST", "/v3/overview/exportThreatTypeProportion", passthrough_body=True, binary=True - ), - "overview_export_ioc_type_proportion": ActionSpec( - "POST", "/v3/overview/exportIocTypeProportion", passthrough_body=True, binary=True - ), - "get_overview_config": ActionSpec("GET", "/v3/setting/overviewConfig"), - "set_overview_config": ActionSpec("PUT", "/v3/setting/overviewConfig", passthrough_body=True), - # status - "device_platform_status": ActionSpec("GET", "/v3/device/platformStatus"), - # OneSIG v2.5.3 文档:模式 A 用 (time, module);模式 B 用 (startTime, endTime, - # module, ifName)。`module` 必传但前端在 index.jsx 初次批拉时传空串即可, - # 因此这里把 `time` 列为强制必填,调用方至少要给一个时间锚。 - "device_system_status": ActionSpec( - "GET", - "/v3/device/systemStatus", - body_keys=["time", "module", "startTime", "endTime", "ifName"], - required=["time"], - ), - "device_network_status": ActionSpec("GET", "/v3/device/networkStatus"), - "common_interface_list": ActionSpec("GET", "/v3/common/interfaceList"), - "basic_cpu_attr": ActionSpec("GET", "/v3/basic/cpuAttr"), - # alert hosts - "alert_host_stat": ActionSpec( - "GET", "/v3/alertHost/stat", body_keys=["startTime", "endTime", "assetGroup"] - ), - "alert_host_tree": ActionSpec("POST", "/v3/alertHost/tree", passthrough_body=True), - "alert_host_list": ActionSpec( - "POST", - "/v3/alertHost/list", - passthrough_body=True, - required=["startTime", "endTime"], - ), - "alert_host_export": ActionSpec( - "POST", "/v3/alertHost/export", passthrough_body=True, binary=True - ), - # OneSIG 文档 monitoringHostdetail.md:必填 startTime + endTime + source - # (source = 当前告警主机 IP,由列表行 `detailData.source` 带入)。 - "alert_host_detail": ActionSpec( - "POST", - "/v3/alertHost/detail", - passthrough_body=True, - required=["startTime", "endTime", "source"], - ), - "alert_host_detail_list": ActionSpec( - "POST", - "/v3/alertHost/detail/list", - passthrough_body=True, - required=["startTime", "endTime", "source"], - ), - "alert_host_detail_export": ActionSpec( - "POST", "/v3/alertHost/detail/export", passthrough_body=True, binary=True - ), - "common_asset_type_list": ActionSpec("GET", "/v3/common/assetTypeList"), - # inbound / outbound threats - "event_inbound_stat": ActionSpec( - "GET", "/v3/event/inbound/stat", body_keys=["startTime", "endTime", "assetGroup"] - ), - "event_inbound_list": ActionSpec( - "POST", - "/v3/event/inbound/list", - passthrough_body=True, - required=["startTime", "endTime"], - ), - "event_inbound_export": ActionSpec( - "POST", "/v3/event/inbound/export", passthrough_body=True, binary=True - ), - # OneSIG 文档 monitoringInboundThreat.md:必填 startTime + endTime; - # 实测仅传时间窗仍可能 1004,因为入站详情弹窗在生产路径上始终带 `threatTag` - # 等行级威胁元数据,建议调用方一并传入 (`threatName` / `threatTag` / - # `threatType` 三选一非空)。 - "event_inbound_detail": ActionSpec( - "POST", - "/v3/event/inbound/detail", - passthrough_body=True, - required=["startTime", "endTime"], - ), - "event_inbound_detail_trend": ActionSpec( - "POST", - "/v3/event/inbound/detail/trend", - passthrough_body=True, - required=["startTime", "endTime"], - ), - "event_inbound_detail_list": ActionSpec( - "POST", - "/v3/event/inbound/detail/list", - passthrough_body=True, - required=["startTime", "endTime"], - ), - "event_inbound_detail_export": ActionSpec( - "POST", "/v3/event/inbound/detail/export", passthrough_body=True, binary=True - ), - "port_protect_group_list": ActionSpec( - "POST", "/v3/portProtectGroup/list", passthrough_body=True - ), - "web_custom_column_set": ActionSpec( - "PUT", "/v3/webCustomColumn/set", passthrough_body=True - ), - "event_outbound_stat": ActionSpec( - "GET", "/v3/event/outbound/stat", body_keys=["startTime", "endTime", "assetGroup"] - ), - "event_outbound_list": ActionSpec( - "POST", - "/v3/event/outbound/list", - passthrough_body=True, - required=["startTime", "endTime"], - ), - "event_outbound_export": ActionSpec( - "POST", "/v3/event/outbound/export", passthrough_body=True, binary=True - ), - # OneSIG 文档 monitoringOutboundThreat.md:必填 startTime + endTime; - # 与入站系列同理,弹窗内联动行上下文(`threatName` 等)后才能稳定返回数据。 - "event_outbound_detail": ActionSpec( - "POST", - "/v3/event/outbound/detail", - passthrough_body=True, - required=["startTime", "endTime"], - ), - "event_outbound_detail_trend": ActionSpec( - "POST", - "/v3/event/outbound/detail/trend", - passthrough_body=True, - required=["startTime", "endTime"], - ), - "event_outbound_detail_list": ActionSpec( - "POST", - "/v3/event/outbound/detail/list", - passthrough_body=True, - required=["startTime", "endTime"], - ), - "event_outbound_detail_export": ActionSpec( - "POST", "/v3/event/outbound/detail/export", passthrough_body=True, binary=True - ), - "set_dnslog_config": ActionSpec( - "PUT", "/v3/setting/dnslogConfig", passthrough_body=True - ), - # report - "get_notice_config": ActionSpec("GET", "/v3/setting/noticeConfig"), - "report_form_create": ActionSpec("POST", "/v3/report/form", passthrough_body=True), - "report_form_list": ActionSpec( - "POST", "/v3/report/form/list", passthrough_body=True - ), - "report_form_download": ActionSpec( - "GET", - "/v3/report/form/download", - body_keys=["uniqueId", "fileName"], - binary=True, - ), - "report_form_delete": ActionSpec("DELETE", "/v3/report/form", passthrough_body=True), - "report_task_list": ActionSpec( - "POST", "/v3/report/task/list", passthrough_body=True - ), - "report_task_create": ActionSpec("POST", "/v3/report/task", passthrough_body=True), - "report_task_update": ActionSpec("PUT", "/v3/report/task", passthrough_body=True), - "report_task_delete": ActionSpec("DELETE", "/v3/report/task", passthrough_body=True), - "report_task_test": ActionSpec("POST", "/v3/report/task/test", passthrough_body=True), - # shared monitoring - "common_asset_group_tree": ActionSpec("GET", "/v3/common/assetGroupTree"), - "ips_rule_create": ActionSpec("POST", "/v3/ips/rule", passthrough_body=True), - "ips_rule_apply": ActionSpec("POST", "/v3/ips/rule/apply", passthrough_body=True), - "ips_ruleset_namelist": ActionSpec("POST", "/v3/ips/ruleset/namelist", passthrough_body=True), - # OneSIG 文档:必填 ruleId + assetIp(用于查询「当前」生效的规则集名称)。 - "ips_ruleset_referred": ActionSpec( - "POST", - "/v3/ips/ruleset/referred", - passthrough_body=True, - required=["ruleId", "assetIp"], - ), - "logaccess_stat": ActionSpec("GET", "/v3/logAccess/stat"), - "get_dnslog_config": ActionSpec("GET", "/v3/setting/dnslogConfig"), -} - -# Strategy ---------------------------------------------------------------- - -STRATEGY_ACTION_SPECS: dict[str, ActionSpec] = { - # whitelist - "whitelist_add": ActionSpec("POST", "/v3/globalWhitelist", passthrough_body=True), - "whitelist_update": ActionSpec("PUT", "/v3/globalWhitelist", passthrough_body=True), - "whitelist_delete": ActionSpec("DELETE", "/v3/globalWhitelist", passthrough_body=True), - "whitelist_export": ActionSpec( - "POST", "/v3/globalWhitelist/export", passthrough_body=True, binary=True - ), - "whitelist_import": ActionSpec( - "POST", "/v3/globalWhitelist/import", passthrough_body=True - ), - "whitelist_template": ActionSpec( - "GET", "/v3/globalWhitelist/template", binary=True - ), - "whitelist_list": ActionSpec("POST", "/v3/globalWhitelist/list", passthrough_body=True), - "whitelist_remove_batch": ActionSpec( - "DELETE", "/v3/globalWhitelist/remove", passthrough_body=True - ), - # blacklist - "blacklist_location_options": ActionSpec("GET", "/v3/blacklist/location"), - "blacklist_add": ActionSpec("POST", "/v3/globalBlacklist", passthrough_body=True), - "blacklist_update": ActionSpec("PUT", "/v3/globalBlacklist", passthrough_body=True), - "blacklist_delete": ActionSpec("DELETE", "/v3/globalBlacklist", passthrough_body=True), - # OneSIG 文档:必填 blackList(待校验的黑名单对象数组)。 - "blacklist_check": ActionSpec( - "POST", - "/v3/globalBlacklist/check", - passthrough_body=True, - required=["blackList"], - ), - "blacklist_export": ActionSpec( - "POST", "/v3/globalBlacklist/export", passthrough_body=True, binary=True - ), - "blacklist_import": ActionSpec("POST", "/v3/globalBlacklist/import", passthrough_body=True), - "blacklist_template": ActionSpec("GET", "/v3/globalBlacklist/template", binary=True), - "blacklist_list": ActionSpec("POST", "/v3/globalBlacklist/list", passthrough_body=True), - "blacklist_remove_batch": ActionSpec( - "DELETE", "/v3/globalBlacklist/remove", passthrough_body=True - ), - # multi-block - # OneSIG 文档:必填 name + startTime + endTime + pageNo + pageSize; - # `name` 缺失时直接 1004。 - "multiblock_executelog_list": ActionSpec( - "POST", - "/v3/multiblock/executelog", - passthrough_body=True, - required=["name", "startTime", "endTime", "pageNo", "pageSize"], - ), - "multiblock_executelog_export": ActionSpec( - "POST", "/v3/multiblock/executelog/export", passthrough_body=True, binary=True - ), - "multiblock_rule_delete": ActionSpec( - "DELETE", "/v3/multiblock/rule", passthrough_body=True - ), - "multiblock_rule_active": ActionSpec( - "POST", "/v3/multiblock/rule/active", passthrough_body=True - ), - "multiblock_rule_dict": ActionSpec( - "POST", "/v3/multiblock/rule/dict", passthrough_body=True - ), - "multiblock_rule_list": ActionSpec( - "POST", "/v3/multiblock/rule/list", passthrough_body=True - ), - # OneSIG 文档:必填 name(多维封锁规则名称)。 - "multiblock_rule_get": ActionSpec( - "POST", - "/v3/multiblock/rule/get", - passthrough_body=True, - required=["name"], - ), - # OneSIG 文档:预运行需带规则核心字段;前端在发起前会移除 blockTime/ - # blockDirection/showCommit。 - "multiblock_rule_preview": ActionSpec( - "POST", - "/v3/multiblock/rule/preview", - passthrough_body=True, - required=[ - "name", - "detectTimeNum", - "detectTimeUnit", - "detectDirection", - "detectGroups", - "blockType", - ], - ), - "multiblock_rule_create": ActionSpec( - "POST", "/v3/multiblock/rule", passthrough_body=True - ), - "multiblock_rule_update": ActionSpec( - "PUT", "/v3/multiblock/rule", passthrough_body=True - ), - # api keys - "apikey_delete": ActionSpec("DELETE", "/v3/apikey", passthrough_body=True), - "apikey_update": ActionSpec("PUT", "/v3/apikey", passthrough_body=True), - "apikey_create": ActionSpec("POST", "/v3/apikey", passthrough_body=True), - # OneSIG 服务端 GET list 接口要求 query 里至少带 pageNo/pageSize, - # 否则统一回 responseCode=1004 "请求数据非法"。 - "apikey_list": ActionSpec( - "GET", "/v3/apikey/list", body_keys=["pageNo", "pageSize"] - ), - # OneSIG 文档 strategyApi.md:query 必填 key + password - # (二次校验登录密码用于查看 secret,敏感字段勿入日志)。 - "apikey_secret": ActionSpec( - "GET", - "/v3/apikey/secret", - body_keys=["key", "password"], - required=["key", "password"], - ), - # syslog auto-blacklist - "auto_blacklist_delete": ActionSpec( - "DELETE", "/v3/autoBlacklist", passthrough_body=True - ), - # OneSIG 文档:必填 name + port + srcIp + protocol + direction - # (步骤 1 → 步骤 2 之间的接入配置重复性校验)。 - "auto_blacklist_check": ActionSpec( - "POST", - "/v3/autoBlacklist/check", - passthrough_body=True, - required=["name", "port", "srcIp", "protocol", "direction"], - ), - "auto_blacklist_create": ActionSpec( - "POST", "/v3/autoBlacklist", passthrough_body=True - ), - "auto_blacklist_update": ActionSpec( - "PUT", "/v3/autoBlacklist", passthrough_body=True - ), - "auto_blacklist_list": ActionSpec( - "POST", "/v3/autoBlacklist/list", passthrough_body=True - ), - # OneSIG 文档:query 仅 `srcIp` 必填(来自 syslog 接入配置的源 IP)。 - "auto_blacklist_trend": ActionSpec( - "GET", - "/v3/autoBlacklist/trend", - body_keys=["srcIp", "startTime", "endTime"], - required=["srcIp"], - ), - # OneSIG 文档:必填 srcIp + protocol + direction(入站/出站)。 - "auto_blacklist_sample": ActionSpec( - "POST", - "/v3/autoBlacklist/sample", - passthrough_body=True, - required=["srcIp", "protocol", "direction"], - ), - # ftp/sftp linkage - "linkage_delete": ActionSpec("DELETE", "/v3/linkage", passthrough_body=True), - "linkage_create": ActionSpec("POST", "/v3/linkage", passthrough_body=True), - "linkage_update": ActionSpec("PUT", "/v3/linkage", passthrough_body=True), - "linkage_enable": ActionSpec("POST", "/v3/linkage/enable", passthrough_body=True), - # 注意:尽管命名是 "info",OneSIG 服务端在响应前会触发一次 FTP/SFTP - # 连通性测试(v2.5.3 实测:传 uniqueId=dummy 直接返回 1309 - # "FTP联通测试失败[dummy]")。换言之这是个有副作用的接口,仅在确 - # 实需要重新探活时再调用,普通"读配置"用 linkage_list 即可。 - # OneSIG 文档:query 必填 uniqueId(联动配置 ID);password 选填, - # 仅在 FTPAccount 查看密钥时拼接。 - # 注意:尽管命名是 "info",OneSIG 服务端在响应前会触发一次 FTP/SFTP - # 连通性测试(v2.5.3 实测:传 uniqueId=dummy 直接返回 1309 - # "FTP联通测试失败[dummy]")。换言之这是个有副作用的接口。 - "linkage_info": ActionSpec( - "GET", - "/v3/linkage/info", - body_keys=["uniqueId", "password"], - required=["uniqueId"], - ), - "linkage_list": ActionSpec("POST", "/v3/linkage/list", passthrough_body=True), - "linkage_template": ActionSpec("GET", "/v3/linkage/template", binary=True), - "linkage_test": ActionSpec("POST", "/v3/linkage/test", passthrough_body=True), - # IPS - "ips_rule_create": ActionSpec("POST", "/v3/ips/rule", passthrough_body=True), - "ips_rule_all": ActionSpec("POST", "/v3/ips/rule/all", passthrough_body=True), - "ips_rule_apply": ActionSpec("POST", "/v3/ips/rule/apply", passthrough_body=True), - "ips_rule_list": ActionSpec("POST", "/v3/ips/rule/list", passthrough_body=True), - "ips_ruleset_create": ActionSpec("POST", "/v3/ips/ruleset", passthrough_body=True), - "ips_ruleset_update": ActionSpec("PUT", "/v3/ips/ruleset", passthrough_body=True), - "ips_ruleset_delete": ActionSpec("DELETE", "/v3/ips/ruleset", passthrough_body=True), - # OneSIG 文档:必填 name(IPS 规则集名称)。 - "ips_ruleset_info": ActionSpec( - "POST", - "/v3/ips/ruleset/info", - passthrough_body=True, - required=["name"], - ), - "ips_ruleset_list": ActionSpec("POST", "/v3/ips/ruleset/list", passthrough_body=True), - "ips_ruleset_namelist": ActionSpec( - "POST", "/v3/ips/ruleset/namelist", passthrough_body=True - ), - "ips_threat_types": ActionSpec("POST", "/v3/ips/threatTypes", passthrough_body=True), - # HTTP protect (HTTP blacklist) - "http_blacklist_delete": ActionSpec( - "DELETE", "/v3/httpBlacklist", passthrough_body=True - ), - "http_blacklist_enable": ActionSpec( - "POST", "/v3/httpBlacklist/enable", passthrough_body=True - ), - "http_blacklist_export": ActionSpec( - "POST", "/v3/httpBlacklist/export", passthrough_body=True, binary=True - ), - "http_blacklist_list": ActionSpec( - "POST", "/v3/httpBlacklist/list", passthrough_body=True - ), - "http_blacklist_create": ActionSpec( - "POST", "/v3/httpBlacklist", passthrough_body=True - ), - "http_blacklist_update": ActionSpec( - "PUT", "/v3/httpBlacklist", passthrough_body=True - ), - "get_advanced_config": ActionSpec("GET", "/v3/setting/advancedConfig"), - "set_advanced_config": ActionSpec("PUT", "/v3/setting/advancedConfig", passthrough_body=True), - "get_xff_config": ActionSpec("GET", "/v3/setting/xffConfig"), - "set_xff_config": ActionSpec("PUT", "/v3/setting/xffConfig", passthrough_body=True), - # port protect groups - "port_protect_group_delete": ActionSpec( - "DELETE", "/v3/portProtectGroup", passthrough_body=True - ), - "port_protect_group_create": ActionSpec( - "POST", "/v3/portProtectGroup", passthrough_body=True - ), - "port_protect_group_update": ActionSpec( - "PUT", "/v3/portProtectGroup", passthrough_body=True - ), - "port_protect_group_clone": ActionSpec( - "POST", "/v3/portProtectGroup/clone", passthrough_body=True - ), - "port_protect_group_default_info": ActionSpec( - "GET", "/v3/portProtectGroup/defaultInfo" - ), - "port_protect_group_list_full": ActionSpec( - "POST", "/v3/portProtectGroup/list", passthrough_body=True - ), - "port_protect_port_delete": ActionSpec( - "DELETE", "/v3/portProtectGroup/port", passthrough_body=True - ), - "port_protect_port_create": ActionSpec( - "POST", "/v3/portProtectGroup/port", passthrough_body=True - ), - "port_protect_port_update": ActionSpec( - "PUT", "/v3/portProtectGroup/port", passthrough_body=True - ), - "port_protect_port_export": ActionSpec( - "POST", "/v3/portProtectGroup/port/export", passthrough_body=True, binary=True - ), - # OneSIG 文档:必填 groupName + pageNo + pageSize;search 超过 32 字符 - # 前端拦截。 - "port_protect_port_list": ActionSpec( - "POST", - "/v3/portProtectGroup/port/list", - passthrough_body=True, - required=["groupName", "pageNo", "pageSize"], - ), - "port_protect_port_onekey_import": ActionSpec( - "POST", "/v3/portProtectGroup/port/onekeyImport", passthrough_body=True - ), - "port_protect_port_onekey_status": ActionSpec( - "GET", "/v3/portProtectGroup/port/onekeyImport" - ), - "port_protect_portinfo": ActionSpec( - "POST", "/v3/portProtectGroup/portinfo", passthrough_body=True - ), - # strategy page (custom protection policy) - "device_onekey_bypass": ActionSpec( - "POST", "/v3/device/onekeyBypass", passthrough_body=True - ), - "protection_policy_delete": ActionSpec( - "DELETE", "/v3/protection/policy", passthrough_body=True - ), - "protection_policy_update": ActionSpec( - "PUT", "/v3/protection/policy", passthrough_body=True - ), - "protection_policy_get": ActionSpec( - "GET", "/v3/protection/policy", body_keys=["uniqueId"] - ), - "protection_policy_tree": ActionSpec("GET", "/v3/protection/policy/tree"), - "set_scan_config": ActionSpec("PUT", "/v3/setting/scanConfig", passthrough_body=True), -} - -# Assets ------------------------------------------------------------------ - -ASSETS_ACTION_SPECS: dict[str, ActionSpec] = { - "asset_delete": ActionSpec("DELETE", "/v3/asset", passthrough_body=True), - "asset_create": ActionSpec("POST", "/v3/asset", passthrough_body=True), - "asset_update": ActionSpec("PUT", "/v3/asset", passthrough_body=True), - "asset_export": ActionSpec("POST", "/v3/asset/export", passthrough_body=True, binary=True), - "asset_group_delete": ActionSpec("DELETE", "/v3/asset/group", passthrough_body=True), - "asset_group_get": ActionSpec("GET", "/v3/asset/group"), - "asset_group_create": ActionSpec("POST", "/v3/asset/group", passthrough_body=True), - "asset_group_update": ActionSpec("PUT", "/v3/asset/group", passthrough_body=True), - "asset_import": ActionSpec( - "POST", "/v3/asset/import", passthrough_body=True, multipart=True - ), - "asset_template": ActionSpec("GET", "/v3/asset/template", binary=True), - "asset_list": ActionSpec("POST", "/v3/asset/list", passthrough_body=True), - "asset_type_delete": ActionSpec("DELETE", "/v3/asset/type", passthrough_body=True), - "asset_type_get": ActionSpec("GET", "/v3/asset/type"), - "asset_type_create": ActionSpec("POST", "/v3/asset/type", passthrough_body=True), - "common_asset_group_tree": ActionSpec("GET", "/v3/common/assetGroupTree"), -} - -# Device ------------------------------------------------------------------ - -DEVICE_ACTION_SPECS: dict[str, ActionSpec] = { - # alert policy - "alert_policy_list": ActionSpec( - "POST", "/v3/alert/policy/list", passthrough_body=True - ), - "alert_policy_enable": ActionSpec( - "POST", "/v3/alert/policy/enable", passthrough_body=True - ), - "alert_policy_delete": ActionSpec( - "DELETE", "/v3/alert/policy", passthrough_body=True - ), - "alert_policy_create": ActionSpec( - "POST", "/v3/alert/policy", passthrough_body=True - ), - "alert_policy_update": ActionSpec( - "PUT", "/v3/alert/policy", passthrough_body=True - ), - "alert_policy_export": ActionSpec( - "POST", "/v3/alert/policy/export", passthrough_body=True, binary=True - ), - # OneSIG 文档:必填 search + type; - # - syslog: search 形如 "UDP:192.168.0.1:514" - # - webhook: search 形如 "type:url" - "alert_policy_find_by_config": ActionSpec( - "POST", - "/v3/alert/policy/findByConfig", - passthrough_body=True, - required=["search", "type"], - ), - "alert_policy_object": ActionSpec( - "POST", "/v3/alert/policy/object", passthrough_body=True - ), - "get_notice_config": ActionSpec("GET", "/v3/setting/noticeConfig"), - "set_notice_config": ActionSpec("PUT", "/v3/setting/noticeConfig", passthrough_body=True), - "get_notice_send_key": ActionSpec("GET", "/v3/setting/noticeConfig/sendKey"), - "test_email": ActionSpec("POST", "/v3/test/email", passthrough_body=True), - "test_syslog": ActionSpec("POST", "/v3/test/syslog", passthrough_body=True), - "test_webhook": ActionSpec("POST", "/v3/test/webhook", passthrough_body=True), - # audit logs - "aclog_stat": ActionSpec("GET", "/v3/aclog/stat", body_keys=["startTime", "endTime"]), - "aclog_list": ActionSpec("POST", "/v3/aclog/list", passthrough_body=True), - "aclog_export": ActionSpec( - "POST", "/v3/aclog/export", passthrough_body=True, binary=True - ), - "aclog_delete": ActionSpec( - "DELETE", - "/v3/aclog", - passthrough_body=True, - encrypt_fields=("password",), - ), - "get_clean_config": ActionSpec("GET", "/v3/setting/cleanConfig"), - "set_clean_config": ActionSpec("PUT", "/v3/setting/cleanConfig", passthrough_body=True), - # users / login mgmt - "user_list": ActionSpec("POST", "/v3/user/list", passthrough_body=True), - "user_export": ActionSpec( - "POST", "/v3/user/export", passthrough_body=True, binary=True - ), - "user_delete": ActionSpec( - "DELETE", - "/v3/user", - passthrough_body=True, - encrypt_fields=("password",), - ), - "user_secret_reset": ActionSpec( - "PUT", - "/v3/user/secret/reset", - passthrough_body=True, - encrypt_fields=("password",), - ), - "user_create": ActionSpec( - "POST", - "/v3/user", - passthrough_body=True, - encrypt_fields=("password", "dupPassword"), - ), - "user_update": ActionSpec("PUT", "/v3/user", passthrough_body=True), - "get_login_config": ActionSpec("GET", "/v3/setting/loginConfig"), - "set_login_config": ActionSpec("PUT", "/v3/setting/loginConfig", passthrough_body=True), - # HTTPS decryption - "get_decrypt_config": ActionSpec("GET", "/v3/setting/decryptConfig"), - "set_decrypt_config": ActionSpec("PUT", "/v3/setting/decryptConfig", passthrough_body=True), - "get_detect_config": ActionSpec("GET", "/v3/setting/detectConfig"), - "set_detect_config": ActionSpec("PUT", "/v3/setting/detectConfig", passthrough_body=True), - "tls_decrypt_policy_list": ActionSpec( - "POST", "/v3/tls/decrypt/policy/list", passthrough_body=True - ), - "tls_decrypt_policy_create": ActionSpec( - "POST", "/v3/tls/decrypt/policy", passthrough_body=True - ), - "tls_decrypt_policy_update": ActionSpec( - "PUT", "/v3/tls/decrypt/policy", passthrough_body=True - ), - "tls_decrypt_policy_enable": ActionSpec( - "POST", "/v3/tls/decrypt/policy/enable", passthrough_body=True - ), - "tls_decrypt_policy_delete": ActionSpec( - "DELETE", "/v3/tls/decrypt/policy", passthrough_body=True - ), - "tls_decrypt_policy_batch": ActionSpec( - "POST", "/v3/tls/decrypt/policy/batch", passthrough_body=True - ), - "tls_cert_list": ActionSpec("POST", "/v3/tls/cert/list", passthrough_body=True), - "tls_cert_create": ActionSpec( - "POST", "/v3/tls/cert", passthrough_body=True, multipart=True, - multipart_file_field="certFile", - ), - "tls_cert_update": ActionSpec( - "PUT", "/v3/tls/cert", passthrough_body=True, multipart=True, - multipart_file_field="certFile", - ), - "tls_cert_delete": ActionSpec("DELETE", "/v3/tls/cert", passthrough_body=True), - "tls_cert_set_default": ActionSpec( - "POST", "/v3/tls/cert/set_default", passthrough_body=True - ), - "tls_detect_list": ActionSpec("POST", "/v3/tls/detect/list", passthrough_body=True), - # OneSIG 文档:必填 server + port + orderBy + sortBy - # (父行 serverAddress/serverPort,固定 desc / updateTime)。 - "tls_detect_list_detail": ActionSpec( - "POST", - "/v3/tls/detect/list/detail", - passthrough_body=True, - required=["server", "port", "orderBy", "sortBy"], - ), - "tls_detect_delete": ActionSpec("DELETE", "/v3/tls/detect", passthrough_body=True), - "tls_detect_group": ActionSpec("POST", "/v3/tls/detect/group", passthrough_body=True), - "tls_detect_group_export": ActionSpec( - "POST", "/v3/tls/detect/group/export", passthrough_body=True, binary=True - ), - "tls_detect_list_export": ActionSpec( - "POST", "/v3/tls/detect/list/export", passthrough_body=True, binary=True - ), - # deploy guide / interface - "interface_list": ActionSpec("GET", "/v3/interface/list"), - "interface_update": ActionSpec( - "PUT", - "/v3/interface", - passthrough_body=True, - encrypt_fields=("password",), - ), - "interface_check_loop": ActionSpec( - "POST", "/v3/interface/check/loop", passthrough_body=True - ), - "interface_relation_list": ActionSpec("GET", "/v3/interface/relation/list"), - # OneSIG 文档:必填 workMode(listen/bridge/vline);不传直接 1004。 - "interface_select_list": ActionSpec( - "GET", - "/v3/interface/select/list", - body_keys=["workMode", "name", "itemName"], - required=["workMode"], - ), - "interface_virtual_line_create": ActionSpec( - "POST", "/v3/interface/virtualLine", passthrough_body=True - ), - "interface_virtual_line_update": ActionSpec( - "PUT", "/v3/interface/virtualLine", passthrough_body=True - ), - "interface_virtual_line_delete": ActionSpec( - "DELETE", "/v3/interface/virtualLine", passthrough_body=True - ), - "interface_listen_create": ActionSpec( - "POST", "/v3/interface/listen", passthrough_body=True - ), - "interface_listen_update": ActionSpec( - "PUT", "/v3/interface/listen", passthrough_body=True - ), - "interface_listen_delete": ActionSpec( - "DELETE", "/v3/interface/listen", passthrough_body=True - ), - "interface_bridge_create": ActionSpec( - "POST", "/v3/interface/bridge", passthrough_body=True - ), - "interface_bridge_update": ActionSpec( - "PUT", "/v3/interface/bridge", passthrough_body=True - ), - "interface_bridge_delete": ActionSpec( - "DELETE", "/v3/interface/bridge", passthrough_body=True - ), - # routes - "route_outif_list": ActionSpec("GET", "/v3/route/outIf/list"), - "route_static_list": ActionSpec("POST", "/v3/route/static/list", passthrough_body=True), - "route_static_create": ActionSpec("POST", "/v3/route/static", passthrough_body=True), - "route_static_update": ActionSpec("PUT", "/v3/route/static", passthrough_body=True), - "route_static_delete": ActionSpec( - "DELETE", "/v3/route/static", passthrough_body=True - ), - "route_table_list": ActionSpec("POST", "/v3/route/table/list", passthrough_body=True), - "ipv6_route_static_list": ActionSpec( - "POST", "/v3/ipv6Route/static/list", passthrough_body=True - ), - "ipv6_route_static_create": ActionSpec( - "POST", "/v3/ipv6Route/static", passthrough_body=True - ), - "ipv6_route_static_update": ActionSpec( - "PUT", "/v3/ipv6Route/static", passthrough_body=True - ), - "ipv6_route_static_delete": ActionSpec( - "DELETE", "/v3/ipv6Route/static", passthrough_body=True - ), - "ipv6_route_table_list": ActionSpec( - "POST", "/v3/ipv6Route/table/list", passthrough_body=True - ), - # DNS config - "get_dns_config": ActionSpec("GET", "/v3/setting/dnsConfig"), - "set_dns_config": ActionSpec("PUT", "/v3/setting/dnsConfig", passthrough_body=True), - "hosts_get": ActionSpec("GET", "/v3/setting/hosts"), - "hosts_create": ActionSpec("POST", "/v3/setting/hosts", passthrough_body=True), - "hosts_update": ActionSpec("PUT", "/v3/setting/hosts", passthrough_body=True), - "hosts_delete": ActionSpec("DELETE", "/v3/setting/hosts", passthrough_body=True), - "test_network": ActionSpec("GET", "/v3/test/network"), - # proxy / agent - "get_proxy_config": ActionSpec("GET", "/v3/setting/proxyConfig"), - "set_proxy_config": ActionSpec("PUT", "/v3/setting/proxyConfig", passthrough_body=True), - "test_proxy": ActionSpec("POST", "/v3/test/proxy", passthrough_body=True), - # HA - "ha_status": ActionSpec("GET", "/v3/ha/status"), - "get_ha_config": ActionSpec("GET", "/v3/setting/haConfig"), - "set_ha_config": ActionSpec("PUT", "/v3/setting/haConfig", passthrough_body=True), - "ha_module_list": ActionSpec("GET", "/v3/ha/moduleList"), - "ha_compare_config": ActionSpec("POST", "/v3/ha/compareConfig", passthrough_body=True), - "ha_switching": ActionSpec("PUT", "/v3/ha/switching", passthrough_body=True), - "ha_sync_config": ActionSpec("POST", "/v3/ha/syncConfig", passthrough_body=True), - # OneSIG 文档:必填 syncId(由 ha_sync_config 返回的任务 ID)。 - "ha_sync_status": ActionSpec( - "GET", "/v3/ha/syncStatus", body_keys=["syncId"], required=["syncId"] - ), - # centralized control (OneCC) - "onecc_status": ActionSpec("GET", "/v3/setting/oneccConfig/status"), - "get_onecc_config": ActionSpec("GET", "/v3/setting/oneccConfig"), - "set_onecc_config": ActionSpec("PUT", "/v3/setting/oneccConfig", passthrough_body=True), - "set_onecc_status": ActionSpec("PUT", "/v3/setting/oneccConfig/status", passthrough_body=True), - "test_onecc": ActionSpec("POST", "/v3/test/onecc", passthrough_body=True), - # device config - "device_quick_bypass": ActionSpec( - "POST", "/v3/device/quickBypass", passthrough_body=True - ), - "device_upgrade_record_list": ActionSpec( - "POST", "/v3/device/upgradeRecord/list", passthrough_body=True - ), - "get_upgrade_config": ActionSpec("GET", "/v3/setting/upgradeConfig"), - "set_upgrade_config": ActionSpec( - "PUT", "/v3/setting/upgradeConfig", passthrough_body=True - ), - "basic_version": ActionSpec("GET", "/v3/basic/version"), - "device_upgrade_info": ActionSpec("GET", "/v3/device/upgradeInfo"), - "device_download_package": ActionSpec( - "POST", "/v3/device/downloadPackage", passthrough_body=True - ), - # /v3/device/upgrade has two flavors: - # * 已下载包升级: JSON body with `name` (Query) + `password` (RSA); - # * 本地上传包: multipart with file + `password`. - # Default to JSON; supply `file_path` to use multipart. - "device_upgrade": ActionSpec( - "POST", - "/v3/device/upgrade", - passthrough_body=True, - encrypt_fields=("password",), - ), - "device_upgrade_upload": ActionSpec( - "POST", - "/v3/device/upgrade", - passthrough_body=True, - multipart=True, - encrypt_fields=("password",), - ), - "system_upgrade": ActionSpec( - "POST", "/v3/system/upgrade", passthrough_body=True, multipart=True - ), - "device_custom_get": ActionSpec("GET", "/v3/device/custom"), - "device_custom_set": ActionSpec("PUT", "/v3/device/custom", passthrough_body=True), - "device_reboot": ActionSpec("POST", "/v3/device/reboot", passthrough_body=True), - "device_shutdown": ActionSpec("POST", "/v3/device/shutdown", passthrough_body=True), - "device_reinit": ActionSpec("POST", "/v3/device/reinit", passthrough_body=True), - "device_system_timezone": ActionSpec("GET", "/v3/device/systemTimeZone"), - "device_system_time_get": ActionSpec("GET", "/v3/device/systemTime"), - "device_system_time_set": ActionSpec("PUT", "/v3/device/systemTime", passthrough_body=True), - "get_storage_config": ActionSpec("GET", "/v3/setting/storageConfig"), - "set_storage_config": ActionSpec("PUT", "/v3/setting/storageConfig", passthrough_body=True), - "backup_recover_progress": ActionSpec("GET", "/v3/backup/recover/progress"), - "backup_list": ActionSpec("POST", "/v3/backup/list", passthrough_body=True), - "backup_create": ActionSpec("POST", "/v3/backup", passthrough_body=True), - "backup_download": ActionSpec( - "GET", "/v3/backup/download", body_keys=["uniqueId"], binary=True - ), - "backup_recover": ActionSpec("POST", "/v3/backup/recover", passthrough_body=True), - "backup_delete": ActionSpec("DELETE", "/v3/backup", passthrough_body=True), - "backup_update": ActionSpec("PUT", "/v3/backup", passthrough_body=True), - "backup_import": ActionSpec( - "POST", "/v3/backup/import", passthrough_body=True, multipart=True - ), - # OneSIG 文档:必填 pageNo + pageSize + type("DNS" 或 "DHCP")。 - "logaccess_list": ActionSpec( - "POST", - "/v3/logAccess/list", - passthrough_body=True, - required=["pageNo", "pageSize", "type"], - ), - "logaccess_delete": ActionSpec("DELETE", "/v3/logAccess", passthrough_body=True), - "logaccess_create": ActionSpec("POST", "/v3/logAccess", passthrough_body=True), - "logaccess_update": ActionSpec("PUT", "/v3/logAccess", passthrough_body=True), - # OneSIG 文档:必填 srcIp + protocol + type("DNS" 或 "DHCP")。 - "logaccess_sample": ActionSpec( - "POST", - "/v3/logAccess/sample", - passthrough_body=True, - required=["srcIp", "protocol", "type"], - ), - "logaccess_test": ActionSpec("POST", "/v3/logAccess/test", passthrough_body=True), - "logaccess_check": ActionSpec("GET", "/v3/logAccess/check", body_keys=["name"]), - # system info - "basic_license_get": ActionSpec("GET", "/v3/basic/license"), - "basic_connect_status": ActionSpec("GET", "/v3/basic/connectStatus"), - "basic_information": ActionSpec("GET", "/v3/basic/information"), - "basic_information_enable": ActionSpec( - "POST", "/v3/basic/information/enable", passthrough_body=True - ), - "basic_information_import": ActionSpec( - "POST", "/v3/basic/information", passthrough_body=True, multipart=True - ), - "basic_license_upload": ActionSpec( - "POST", "/v3/basic/license", passthrough_body=True, multipart=True - ), - "mdr_service_status": ActionSpec("GET", "/v3/mdrService/status"), - "mdr_service_enable": ActionSpec( - "PUT", "/v3/mdrService/enable", passthrough_body=True - ), - # system diagnosis - "device_coredump_list": ActionSpec("GET", "/v3/device/coredump"), - "device_coredump_download": ActionSpec( - "POST", "/v3/device/coredumpDownload", passthrough_body=True, binary=True - ), - "device_coredump_delete": ActionSpec( - "DELETE", "/v3/device/coredump", passthrough_body=True - ), - "device_pcap_get": ActionSpec("GET", "/v3/device/pcap"), - "device_pcap_set": ActionSpec("PUT", "/v3/device/pcap", passthrough_body=True), - "device_pcap_file_list": ActionSpec("GET", "/v3/device/pcapFile"), - "device_pcap_download": ActionSpec( - "POST", "/v3/device/pcapDownload", passthrough_body=True, binary=True - ), - "device_pcap_file_delete": ActionSpec( - "DELETE", "/v3/device/pcapFile", passthrough_body=True - ), -} - -# Helper ------------------------------------------------------------------ - -HELPER_ACTION_SPECS: dict[str, ActionSpec] = { - "document_list": ActionSpec("POST", "/v3/document/list", passthrough_body=True), - # OneSIG 文档 helperDocs.md:query 必填 `id`(来自文档列表项), - # 而非 `fileName`。返回值是路径字符串(非对象),前端拼接 baseUrl 后 open。 - "document_preview": ActionSpec( - "GET", - "/v3/document/preview", - body_keys=["id"], - required=["id"], - ), - "product_news_get": ActionSpec("GET", "/v3/product/news"), - "product_news_mark_read": ActionSpec( - "PUT", "/v3/product/news", passthrough_body=True - ), - "product_version": ActionSpec("GET", "/v3/product/version"), - "product_issue": ActionSpec("POST", "/v3/product/issue", passthrough_body=True), -} - - -GROUP_SPECS: dict[str, dict[str, ActionSpec]] = { - "login": LOGIN_ACTION_SPECS, - "monitoring": MONITORING_ACTION_SPECS, - "strategy": STRATEGY_ACTION_SPECS, - "assets": ASSETS_ACTION_SPECS, - "device": DEVICE_ACTION_SPECS, - "helper": HELPER_ACTION_SPECS, -} - -# Lightweight read-only actions used by `action="test"` for connectivity check. -_CONNECTIVITY_TEST_ACTIONS: dict[str, str] = { - "login": "get_account", - "monitoring": "common_threat_type_list", - "strategy": "blacklist_location_options", - "assets": "common_asset_group_tree", - "device": "basic_version", - "helper": "product_version", -} - - -# --------------------------------------------------------------------------- -# Output handling -# --------------------------------------------------------------------------- - - -def _outputs_dir() -> str: - """Resolve the daily outputs directory used to persist binary downloads.""" - import datetime - from pathlib import Path - - try: - from flocks.workspace.manager import WorkspaceManager - - ws = WorkspaceManager.get_instance() - base = Path(ws.get_workspace_dir()) / "outputs" / datetime.date.today().isoformat() - except Exception: - base = Path.home() / ".flocks" / "workspace" / "outputs" / datetime.date.today().isoformat() - base.mkdir(parents=True, exist_ok=True) - return str(base) - - -def _save_binary(path: str, body: bytes, content_type: str) -> str: - import datetime - from pathlib import Path - - safe_name = path.strip("/").replace("/", "_") or "download" - ext = "" - ct = content_type.lower() - if "csv" in ct: - ext = ".csv" - elif "excel" in ct or "spreadsheet" in ct or "xlsx" in ct: - ext = ".xlsx" - elif "zip" in ct: - ext = ".zip" - elif "pdf" in ct: - ext = ".pdf" - elif "octet-stream" in ct: - ext = ".bin" - timestamp = datetime.datetime.now().strftime("%Y%m%dT%H%M%S") - target = Path(_outputs_dir()) / f"onesig_v2_5_3_D20250710_{safe_name}_{timestamp}{ext}" - target.write_bytes(body) - return str(target) - - -def _envelope_to_result(action: str, envelope: dict[str, Any]) -> ToolResult: - metadata = {"source": "OneSIG", "api": action} - response_code = envelope.get("responseCode") - if response_code is not None and response_code != _RESPONSE_CODE_OK: - msg = envelope.get("verboseMsg") or envelope.get("verbose_msg") or "Unknown error" - return ToolResult( - success=False, - error=f"OneSIG API error (responseCode={response_code}): {msg}", - output=envelope, - metadata=metadata, - ) - if "data" in envelope: - return ToolResult(success=True, output=envelope.get("data"), metadata=metadata) - return ToolResult(success=True, output=envelope, metadata=metadata) - - -# --------------------------------------------------------------------------- -# Dispatch -# --------------------------------------------------------------------------- - - -async def _encrypt_body_fields( - session: "OneSIGSession", - spec: ActionSpec, - body: Optional[dict[str, Any]], -) -> Optional[dict[str, Any]]: - """Replace each plaintext field listed in ``spec.encrypt_fields`` with its - RSA-OAEP ciphertext (Base64). A fresh pubkey is fetched per call to match - the front-end's ``clearRSACache`` behavior. No-op if body is None.""" - if not spec.encrypt_fields or body is None or not isinstance(body, dict): - return body - has_target = any(_has_value(body.get(f)) for f in spec.encrypt_fields) - if not has_target: - return body - pubkey_resp = await session._raw_request_json("GET", "/v3/pubkey") - pubkey = (pubkey_resp or {}).get("data", {}).get("pubkey") - if not pubkey: - raise ValueError( - f"无法从 /v3/pubkey 获取 RSA 公钥用于加密字段 {list(spec.encrypt_fields)}: {pubkey_resp!r}" - ) - out = dict(body) - for field in spec.encrypt_fields: - plain = out.get(field) - if isinstance(plain, str) and plain: - out[field] = _rsa_oaep_encrypt(pubkey, plain, session.config.oaep_hash) - return out - - -def _build_form_data( - spec: ActionSpec, - body: Optional[dict[str, Any]], - file_path: Optional[str], -) -> "aiohttp.FormData": - """Assemble a multipart/form-data payload: the file under - ``spec.multipart_file_field`` plus any non-file fields from ``body``.""" - if not file_path: - raise ValueError( - f"multipart 接口 {spec.path} 需要 `file_path` 参数指向待上传的本地文件" - ) - from pathlib import Path as _Path - - fp = _Path(file_path).expanduser() - if not fp.is_file(): - raise ValueError(f"file_path 指向的文件不存在或不是常规文件:{fp}") - - form = aiohttp.FormData() - fname = fp.name - # Stream the file via an open handle; aiohttp closes it on send completion. - form.add_field( - spec.multipart_file_field, - fp.open("rb"), - filename=fname, - content_type="application/octet-stream", - ) - if isinstance(body, dict): - for k, v in body.items(): - if v is None: - continue - if isinstance(v, (dict, list, tuple)): - import json as _json - - form.add_field(k, _json.dumps(v, ensure_ascii=False)) - elif isinstance(v, bool): - form.add_field(k, "true" if v else "false") - else: - form.add_field(k, str(v)) - return form - - -async def _execute_action( - group: str, - action: str, - params: dict[str, Any], -) -> ToolResult: - spec_map = GROUP_SPECS[group] - spec = spec_map[action] - - validation_error = _validate_required(spec, action, params) - if validation_error: - return ToolResult(success=False, error=validation_error) - - try: - config = _resolve_runtime_config() - except ValueError as exc: - return ToolResult(success=False, error=str(exc)) - - captcha = params.get("captcha") - totp = params.get("totp") - - session = await _get_session(config) - try: - query, body = spec.build_request(params) - body = await _encrypt_body_fields(session, spec, body) - - if spec.multipart: - form = _build_form_data(spec, body, params.get("file_path")) - status, envelope, body_bytes, content_type = await session.request( - spec.method, - spec.path, - params=query, - form_data=form, - captcha=captcha, - totp=totp, - ) - else: - status, envelope, body_bytes, content_type = await session.request( - spec.method, - spec.path, - params=query, - json_body=body, - captcha=captcha, - totp=totp, - ) - except ValueError as exc: - return ToolResult(success=False, error=str(exc)) - except aiohttp.ClientError as exc: - return ToolResult(success=False, error=f"Request failed: {exc}") - except Exception as exc: # pragma: no cover - defensive - return ToolResult(success=False, error=f"Unexpected error: {exc}") - - metadata: dict[str, Any] = { - "source": "OneSIG", - "api": action, - "method": spec.method, - "path": spec.path, - "http_status": status, - } - if isinstance(envelope, dict): - if "responseCode" in envelope: - metadata["response_code"] = envelope.get("responseCode") - verbose_msg = envelope.get("verboseMsg") or envelope.get("verbose_msg") - if verbose_msg: - metadata["verbose_msg"] = verbose_msg - - if spec.binary or (body_bytes and not envelope): - if status >= 400: - return ToolResult( - success=False, - error=f"HTTP {status} from {spec.path}", - metadata=metadata, - ) - saved_path = _save_binary(spec.path, body_bytes, content_type) - metadata["saved_path"] = saved_path - metadata["binary_size"] = len(body_bytes) - metadata["content_type"] = content_type - return ToolResult( - success=True, - output={ - "saved_path": saved_path, - "size": len(body_bytes), - "content_type": content_type, - }, - metadata=metadata, - ) - - if status >= 400 and not envelope: - return ToolResult( - success=False, - error=f"HTTP {status} from {spec.path}", - metadata=metadata, - ) - - result = _envelope_to_result(action, envelope or {}) - merged_meta = dict(result.metadata or {}) - merged_meta.update(metadata) - result.metadata = merged_meta - return result - - -async def _dispatch_group( - ctx: ToolContext, - group: str, - action: str, - **params: Any, -) -> ToolResult: - del ctx - if action == "test": - test_action = _CONNECTIVITY_TEST_ACTIONS.get(group) - if test_action: - return await _execute_action(group, test_action, params) - spec_map = GROUP_SPECS[group] - if action not in spec_map and action not in {"login", "logout", "change_password"}: - available = ", ".join(sorted(spec_map)) - return ToolResult( - success=False, - error=f"Unsupported {group} action: {action}. Available actions: {available}", - ) - if group == "login": - if action == "login": - return await _login_action(params) - if action == "logout": - return await _logout_action() - if action == "change_password": - return await _change_password_action(params) - return await _execute_action(group, action, params) - - -async def _login_action(params: dict[str, Any]) -> ToolResult: - try: - config = _resolve_runtime_config() - except ValueError as exc: - return ToolResult(success=False, error=str(exc)) - session = await _get_session(config) - try: - account = await session.login( - captcha=params.get("captcha"), - totp=params.get("totp"), - force=True, - ) - except ValueError as exc: - return ToolResult(success=False, error=str(exc)) - return ToolResult( - success=True, - output={"account": account, "message": "Logged in to OneSIG"}, - metadata={"source": "OneSIG", "api": "login"}, - ) - - -async def _logout_action() -> ToolResult: - try: - config = _resolve_runtime_config() - except ValueError as exc: - return ToolResult(success=False, error=str(exc)) - session = await _get_session(config) - try: - envelope = await session.logout() - except ValueError as exc: - return ToolResult(success=False, error=str(exc)) - finally: - await session.close() - async with _SESSIONS_LOCK: - _SESSIONS.pop(config.session_key, None) - return _envelope_to_result("logout", envelope or {}) - - -async def _change_password_action(params: dict[str, Any]) -> ToolResult: - """Change the current user's password (RSA-OAEP encrypts each field).""" - new_password = params.get("new_password") or params.get("newPassword") - old_password = params.get("old_password") or params.get("oldPassword") - dup_password = params.get("dup_password") or params.get("dupPassword") or new_password - if not new_password: - return ToolResult( - success=False, - error="change_password 缺少 new_password 参数", - ) - try: - config = _resolve_runtime_config() - except ValueError as exc: - return ToolResult(success=False, error=str(exc)) - session = await _get_session(config) - try: - if not session._logged_in: - await session.login(captcha=params.get("captcha"), totp=params.get("totp")) - pubkey_resp = await session._raw_request_json("GET", "/v3/pubkey") - pubkey = (pubkey_resp or {}).get("data", {}).get("pubkey") - if not pubkey: - return ToolResult( - success=False, - error=f"无法获取 RSA 公钥用于改密:{pubkey_resp!r}", - ) - body: dict[str, Any] = { - "username": params.get("username") or config.username, - "newPassword": _rsa_oaep_encrypt(pubkey, new_password, config.oaep_hash), - "dupPassword": _rsa_oaep_encrypt(pubkey, dup_password, config.oaep_hash), - } - if old_password: - body["oldPassword"] = _rsa_oaep_encrypt(pubkey, old_password, config.oaep_hash) - status, envelope, _, _ = await session.request( - "PUT", "/v3/user/password", json_body=body - ) - except ValueError as exc: - return ToolResult(success=False, error=str(exc)) - if status >= 400 and not envelope: - return ToolResult(success=False, error=f"HTTP {status} from /v3/user/password") - return _envelope_to_result("change_password", envelope or {}) - - -# --------------------------------------------------------------------------- -# Public group entry points (referenced from YAML handler stanzas) -# --------------------------------------------------------------------------- - - -async def login(ctx: ToolContext, action: str = "login", **params: Any) -> ToolResult: - return await _dispatch_group(ctx, "login", action, **params) - - -async def monitoring(ctx: ToolContext, action: str, **params: Any) -> ToolResult: - return await _dispatch_group(ctx, "monitoring", action, **params) - - -async def strategy(ctx: ToolContext, action: str, **params: Any) -> ToolResult: - return await _dispatch_group(ctx, "strategy", action, **params) - - -async def assets(ctx: ToolContext, action: str, **params: Any) -> ToolResult: - return await _dispatch_group(ctx, "assets", action, **params) - - -async def device(ctx: ToolContext, action: str, **params: Any) -> ToolResult: - return await _dispatch_group(ctx, "device", action, **params) - - -async def helper(ctx: ToolContext, action: str, **params: Any) -> ToolResult: - return await _dispatch_group(ctx, "helper", action, **params) diff --git a/.flocks/flockshub/plugins/tools/api/onesig_v2_5_3_D20250710/onesig_v2_5_3_D20250710_assets.yaml b/.flocks/flockshub/plugins/tools/api/onesig_v2_5_3_D20250710/onesig_v2_5_3_D20250710_assets.yaml deleted file mode 100644 index 7de1f2857..000000000 --- a/.flocks/flockshub/plugins/tools/api/onesig_v2_5_3_D20250710/onesig_v2_5_3_D20250710_assets.yaml +++ /dev/null @@ -1,90 +0,0 @@ -name: onesig_v2_5_3_D20250710_assets -description: > - OneSIG asset management grouped tool. Use the `action` parameter to manage - assets, asset groups, asset types, and import / export workflows under - `/assets/segment`. -description_cn: > - OneSIG 资产管理分组工具。通过 `action` 调用资产、资产组、资产类型以及导入/导出 - 相关接口,对应 Web 控制台 `/assets/segment` 资产配置页。 -category: custom -enabled: true -requires_confirmation: true -provider: onesig_v2_5_3_D20250710_api -inputSchema: - type: object - properties: - action: - type: string - description: | - Assets 分组动作名: - - asset_delete DELETE /v3/asset 删除资产 - - asset_create POST /v3/asset 新增资产 - - asset_update PUT /v3/asset 更新资产 - - asset_export POST /v3/asset/export 导出资产(文件) - - asset_group_delete DELETE /v3/asset/group 删除资产组 - - asset_group_get GET /v3/asset/group 查询资产组 - - asset_group_create POST /v3/asset/group 新增资产组 - - asset_group_update PUT /v3/asset/group 更新资产组 - - asset_import POST /v3/asset/import 导入资产(multipart:传 file_path) - - asset_template GET /v3/asset/template 导入模板(文件) - - asset_list POST /v3/asset/list 分页查询资产列表 - - asset_type_delete DELETE /v3/asset/type 删除资产类型 - - asset_type_get GET /v3/asset/type 查询资产类型 - - asset_type_create POST /v3/asset/type 新增资产类型 - - common_asset_group_tree GET /v3/common/assetGroupTree 资产组树 - enum: - - asset_delete - - asset_create - - asset_update - - asset_export - - asset_group_delete - - asset_group_get - - asset_group_create - - asset_group_update - - asset_import - - asset_template - - asset_list - - asset_type_delete - - asset_type_get - - asset_type_create - - common_asset_group_tree - uniqueId: - type: string - description: 资产/资产组/资产类型 ID - uniqueIds: - type: array - items: - type: string - description: 批量删除使用的 ID 列表 - name: - type: string - description: 名称 - ip: - type: string - description: IP / CIDR / 网段 - assetGroup: - type: string - description: 资产组路径 - assetType: - type: string - description: 资产类型 - pageNo: - type: integer - default: 1 - pageSize: - type: integer - default: 20 - search: - type: string - description: 模糊查询关键字 - file_path: - type: string - description: | - 本地待上传文件的绝对路径;仅 multipart 上传类动作使用: - - asset_import .csv 资产导入文件(字段名 `file`) - required: - - action -handler: - type: script - script_file: onesig_v2_5_3_D20250710.handler.py - function: assets diff --git a/.flocks/flockshub/plugins/tools/api/onesig_v2_5_3_D20250710/onesig_v2_5_3_D20250710_device.yaml b/.flocks/flockshub/plugins/tools/api/onesig_v2_5_3_D20250710/onesig_v2_5_3_D20250710_device.yaml deleted file mode 100644 index 157be8e46..000000000 --- a/.flocks/flockshub/plugins/tools/api/onesig_v2_5_3_D20250710/onesig_v2_5_3_D20250710_device.yaml +++ /dev/null @@ -1,400 +0,0 @@ -name: onesig_v2_5_3_D20250710_device -description: > - OneSIG device management grouped tool. Use the `action` parameter to access - alert / notification policies, audit logs, user / login management, HTTPS - decryption, deployment guide, network interfaces, routes, DNS, HA, OneCC - (centralized control), system info, license, MDR service, system diagnosis, - and the device configuration / backup / upgrade flows under `/device/*`. -description_cn: > - OneSIG 平台管理分组工具。通过 `action` 调用告警/通知策略、管理日志、用户/登录安全 - 策略、HTTPS 解密、部署引导、接口、路由、DNS、HA 高可用、一体化管控、系统信息、 - 许可证、MDR 服务、设备诊断以及备份/升级等接口(覆盖 Web 控制台 `/device/*` 子页)。 -category: custom -enabled: true -requires_confirmation: true -provider: onesig_v2_5_3_D20250710_api -inputSchema: - type: object - properties: - action: - type: string - description: | - Device 分组动作名(按 UI 子页归类): - - # 通知配置 (/device/alert) - - alert_policy_list POST /v3/alert/policy/list - - alert_policy_enable POST /v3/alert/policy/enable - - alert_policy_delete DELETE /v3/alert/policy - - alert_policy_create POST /v3/alert/policy - - alert_policy_update PUT /v3/alert/policy - - alert_policy_export POST /v3/alert/policy/export (文件) - - alert_policy_find_by_config POST /v3/alert/policy/findByConfig - - alert_policy_object POST /v3/alert/policy/object - - get_notice_config GET /v3/setting/noticeConfig - - set_notice_config PUT /v3/setting/noticeConfig - - get_notice_send_key GET /v3/setting/noticeConfig/sendKey - - test_email POST /v3/test/email - - test_syslog POST /v3/test/syslog - - test_webhook POST /v3/test/webhook - - # 审计日志 (/device/audit) - - aclog_stat GET /v3/aclog/stat - - aclog_list POST /v3/aclog/list - - aclog_export POST /v3/aclog/export (文件) - - aclog_delete DELETE /v3/aclog (password 自动 RSA 加密) - - get_clean_config GET /v3/setting/cleanConfig - - set_clean_config PUT /v3/setting/cleanConfig - - # 登录管理 (/device/loginManagement) - - user_list POST /v3/user/list - - user_export POST /v3/user/export (文件) - - user_delete DELETE /v3/user (password 自动 RSA 加密) - - user_secret_reset PUT /v3/user/secret/reset (password 自动 RSA 加密) - - user_create POST /v3/user (password / dupPassword 自动 RSA 加密) - - user_update PUT /v3/user - - get_login_config GET /v3/setting/loginConfig - - set_login_config PUT /v3/setting/loginConfig - - # HTTPS 解密 (/device/httpsDecryption) - - get_decrypt_config GET /v3/setting/decryptConfig - - set_decrypt_config PUT /v3/setting/decryptConfig - - get_detect_config GET /v3/setting/detectConfig - - set_detect_config PUT /v3/setting/detectConfig - - tls_decrypt_policy_list POST /v3/tls/decrypt/policy/list - - tls_decrypt_policy_create POST /v3/tls/decrypt/policy - - tls_decrypt_policy_update PUT /v3/tls/decrypt/policy - - tls_decrypt_policy_enable POST /v3/tls/decrypt/policy/enable - - tls_decrypt_policy_delete DELETE /v3/tls/decrypt/policy - - tls_decrypt_policy_batch POST /v3/tls/decrypt/policy/batch - - tls_cert_list POST /v3/tls/cert/list - - tls_cert_create POST /v3/tls/cert (multipart:file_path 指向 .crt/.pem,字段名 certFile) - - tls_cert_update PUT /v3/tls/cert (multipart:同 tls_cert_create,body 需带 uniqueId) - - tls_cert_delete DELETE /v3/tls/cert - - tls_cert_set_default POST /v3/tls/cert/set_default - - tls_detect_list POST /v3/tls/detect/list - - tls_detect_list_detail POST /v3/tls/detect/list/detail - - tls_detect_delete DELETE /v3/tls/detect - - tls_detect_group POST /v3/tls/detect/group - - tls_detect_group_export POST /v3/tls/detect/group/export (文件) - - tls_detect_list_export POST /v3/tls/detect/list/export (文件) - - # 接口 / 部署引导 (/device/interface, /device/deployguide) - - interface_list GET /v3/interface/list - - interface_update PUT /v3/interface (启停场景 password 自动 RSA 加密) - - interface_check_loop POST /v3/interface/check/loop - - interface_relation_list GET /v3/interface/relation/list - - interface_select_list GET /v3/interface/select/list - - interface_virtual_line_create POST /v3/interface/virtualLine - - interface_virtual_line_update PUT /v3/interface/virtualLine - - interface_virtual_line_delete DELETE /v3/interface/virtualLine - - interface_listen_create POST /v3/interface/listen - - interface_listen_update PUT /v3/interface/listen - - interface_listen_delete DELETE /v3/interface/listen - - interface_bridge_create POST /v3/interface/bridge - - interface_bridge_update PUT /v3/interface/bridge - - interface_bridge_delete DELETE /v3/interface/bridge - - # 路由 (/device/route) - - route_outif_list GET /v3/route/outIf/list - - route_static_list POST /v3/route/static/list - - route_static_create POST /v3/route/static - - route_static_update PUT /v3/route/static - - route_static_delete DELETE /v3/route/static - - route_table_list POST /v3/route/table/list - - ipv6_route_static_list POST /v3/ipv6Route/static/list - - ipv6_route_static_create POST /v3/ipv6Route/static - - ipv6_route_static_update PUT /v3/ipv6Route/static - - ipv6_route_static_delete DELETE /v3/ipv6Route/static - - ipv6_route_table_list POST /v3/ipv6Route/table/list - - # DNS 配置 (/device/system_dns) - - get_dns_config GET /v3/setting/dnsConfig - - set_dns_config PUT /v3/setting/dnsConfig - - hosts_get GET /v3/setting/hosts - - hosts_create POST /v3/setting/hosts - - hosts_update PUT /v3/setting/hosts - - hosts_delete DELETE /v3/setting/hosts - - test_network GET /v3/test/network - - # 代理 (/device/agent) - - get_proxy_config GET /v3/setting/proxyConfig - - set_proxy_config PUT /v3/setting/proxyConfig - - test_proxy POST /v3/test/proxy - - # 高可用 (/device/high_availability) - - ha_status GET /v3/ha/status - - get_ha_config GET /v3/setting/haConfig - - set_ha_config PUT /v3/setting/haConfig - - ha_module_list GET /v3/ha/moduleList - - ha_compare_config POST /v3/ha/compareConfig - - ha_switching PUT /v3/ha/switching - - ha_sync_config POST /v3/ha/syncConfig - - ha_sync_status GET /v3/ha/syncStatus - - # 集中管控 (/device/centralized_control) - - onecc_status GET /v3/setting/oneccConfig/status - - get_onecc_config GET /v3/setting/oneccConfig - - set_onecc_config PUT /v3/setting/oneccConfig - - set_onecc_status PUT /v3/setting/oneccConfig/status - - test_onecc POST /v3/test/onecc - - # 设备配置 / 升级 / 备份 / 日志外发 (/device/deviceConfig) - - device_quick_bypass POST /v3/device/quickBypass - - device_upgrade_record_list POST /v3/device/upgradeRecord/list - - get_upgrade_config GET /v3/setting/upgradeConfig - - set_upgrade_config PUT /v3/setting/upgradeConfig - - basic_version GET /v3/basic/version - - device_upgrade_info GET /v3/device/upgradeInfo - - device_download_package POST /v3/device/downloadPackage - - device_upgrade POST /v3/device/upgrade (已下载包升级;password 自动 RSA 加密) - - device_upgrade_upload POST /v3/device/upgrade (本地上传包升级;multipart + password 自动 RSA 加密) - - system_upgrade POST /v3/system/upgrade (multipart:file_path 指向系统升级包) - - device_custom_get GET /v3/device/custom - - device_custom_set PUT /v3/device/custom - - device_reboot POST /v3/device/reboot - - device_shutdown POST /v3/device/shutdown - - device_reinit POST /v3/device/reinit - - device_system_timezone GET /v3/device/systemTimeZone - - device_system_time_get GET /v3/device/systemTime - - device_system_time_set PUT /v3/device/systemTime - - get_storage_config GET /v3/setting/storageConfig - - set_storage_config PUT /v3/setting/storageConfig - - backup_recover_progress GET /v3/backup/recover/progress - - backup_list POST /v3/backup/list - - backup_create POST /v3/backup - - backup_download GET /v3/backup/download (文件) - - backup_recover POST /v3/backup/recover - - backup_delete DELETE /v3/backup - - backup_update PUT /v3/backup - - backup_import POST /v3/backup/import (multipart:file_path 指向备份包) - - logaccess_list POST /v3/logAccess/list - - logaccess_delete DELETE /v3/logAccess - - logaccess_create POST /v3/logAccess - - logaccess_update PUT /v3/logAccess - - logaccess_sample POST /v3/logAccess/sample - - logaccess_test POST /v3/logAccess/test - - logaccess_check GET /v3/logAccess/check - - # 基本信息 (/device/system_info) - - basic_license_get GET /v3/basic/license - - basic_connect_status GET /v3/basic/connectStatus - - basic_information GET /v3/basic/information - - basic_information_enable POST /v3/basic/information/enable - - basic_information_import POST /v3/basic/information (multipart:file_path 指向离线情报库更新包,Query 需 name=...) - - basic_license_upload POST /v3/basic/license (multipart:file_path 指向授权文件) - - mdr_service_status GET /v3/mdrService/status - - mdr_service_enable PUT /v3/mdrService/enable - - # 设备诊断 (/device/system_diagnosis) - - device_coredump_list GET /v3/device/coredump - - device_coredump_download POST /v3/device/coredumpDownload (文件) - - device_coredump_delete DELETE /v3/device/coredump - - device_pcap_get GET /v3/device/pcap - - device_pcap_set PUT /v3/device/pcap - - device_pcap_file_list GET /v3/device/pcapFile - - device_pcap_download POST /v3/device/pcapDownload (文件) - - device_pcap_file_delete DELETE /v3/device/pcapFile - enum: - - alert_policy_list - - alert_policy_enable - - alert_policy_delete - - alert_policy_create - - alert_policy_update - - alert_policy_export - - alert_policy_find_by_config - - alert_policy_object - - get_notice_config - - set_notice_config - - get_notice_send_key - - test_email - - test_syslog - - test_webhook - - aclog_stat - - aclog_list - - aclog_export - - aclog_delete - - get_clean_config - - set_clean_config - - user_list - - user_export - - user_delete - - user_secret_reset - - user_create - - user_update - - get_login_config - - set_login_config - - get_decrypt_config - - set_decrypt_config - - get_detect_config - - set_detect_config - - tls_decrypt_policy_list - - tls_decrypt_policy_create - - tls_decrypt_policy_update - - tls_decrypt_policy_enable - - tls_decrypt_policy_delete - - tls_decrypt_policy_batch - - tls_cert_list - - tls_cert_create - - tls_cert_update - - tls_cert_delete - - tls_cert_set_default - - tls_detect_list - - tls_detect_list_detail - - tls_detect_delete - - tls_detect_group - - tls_detect_group_export - - tls_detect_list_export - - interface_list - - interface_update - - interface_check_loop - - interface_relation_list - - interface_select_list - - interface_virtual_line_create - - interface_virtual_line_update - - interface_virtual_line_delete - - interface_listen_create - - interface_listen_update - - interface_listen_delete - - interface_bridge_create - - interface_bridge_update - - interface_bridge_delete - - route_outif_list - - route_static_list - - route_static_create - - route_static_update - - route_static_delete - - route_table_list - - ipv6_route_static_list - - ipv6_route_static_create - - ipv6_route_static_update - - ipv6_route_static_delete - - ipv6_route_table_list - - get_dns_config - - set_dns_config - - hosts_get - - hosts_create - - hosts_update - - hosts_delete - - test_network - - get_proxy_config - - set_proxy_config - - test_proxy - - ha_status - - get_ha_config - - set_ha_config - - ha_module_list - - ha_compare_config - - ha_switching - - ha_sync_config - - ha_sync_status - - onecc_status - - get_onecc_config - - set_onecc_config - - set_onecc_status - - test_onecc - - device_quick_bypass - - device_upgrade_record_list - - get_upgrade_config - - set_upgrade_config - - basic_version - - device_upgrade_info - - device_download_package - - device_upgrade - - device_upgrade_upload - - system_upgrade - - device_custom_get - - device_custom_set - - device_reboot - - device_shutdown - - device_reinit - - device_system_timezone - - device_system_time_get - - device_system_time_set - - get_storage_config - - set_storage_config - - backup_recover_progress - - backup_list - - backup_create - - backup_download - - backup_recover - - backup_delete - - backup_update - - backup_import - - logaccess_list - - logaccess_delete - - logaccess_create - - logaccess_update - - logaccess_sample - - logaccess_test - - logaccess_check - - basic_license_get - - basic_connect_status - - basic_information - - basic_information_enable - - basic_information_import - - basic_license_upload - - mdr_service_status - - mdr_service_enable - - device_coredump_list - - device_coredump_download - - device_coredump_delete - - device_pcap_get - - device_pcap_set - - device_pcap_file_list - - device_pcap_download - - device_pcap_file_delete - uniqueId: - type: string - description: 资源唯一 ID(删除/下载等场景) - uniqueIds: - type: array - items: - type: string - description: 批量删除使用的 ID 列表 - pageNo: - type: integer - default: 1 - pageSize: - type: integer - default: 20 - startTime: - type: integer - description: 起始时间戳(管理日志统计 / 列表 / 导出场景使用) - endTime: - type: integer - description: 结束时间戳 - name: - type: string - description: 名称(用户名、配置项名称等) - enable: - type: boolean - description: 启停标识 - password: - type: string - description: | - 敏感写操作所需的当前用户登录密码(明文)。处理器会自动用 `/v3/pubkey` - 最新公钥做 RSA-OAEP 加密后再发送,无需调用方手动加密。 - 适用:aclog_delete、user_delete、user_secret_reset、user_create - (同时配合 dupPassword)、interface_update(接口启停)、 - device_upgrade、device_upgrade_upload。 - dupPassword: - type: string - description: | - 新增用户(user_create)使用的二次确认密码(明文)。处理器会与 - `password` 一起用同一公钥 RSA-OAEP 加密。 - file_path: - type: string - description: | - 本地待上传文件的绝对路径;仅 multipart 上传类动作使用: - - tls_cert_create / tls_cert_update .crt/.pem 证书文件(字段名 `certFile`) - - basic_information_import 离线情报库更新包(字段名 `file`,Query 需 `name`) - - basic_license_upload 授权 license 文件(字段名 `file`) - - system_upgrade 系统升级包(字段名 `file`) - - device_upgrade_upload 设备升级包(字段名 `file`,body 同时要求 password) - - backup_import 配置备份包(字段名 `file`) - required: - - action -handler: - type: script - script_file: onesig_v2_5_3_D20250710.handler.py - function: device diff --git a/.flocks/flockshub/plugins/tools/api/onesig_v2_5_3_D20250710/onesig_v2_5_3_D20250710_helper.yaml b/.flocks/flockshub/plugins/tools/api/onesig_v2_5_3_D20250710/onesig_v2_5_3_D20250710_helper.yaml deleted file mode 100644 index be0c6f9a0..000000000 --- a/.flocks/flockshub/plugins/tools/api/onesig_v2_5_3_D20250710/onesig_v2_5_3_D20250710_helper.yaml +++ /dev/null @@ -1,56 +0,0 @@ -name: onesig_v2_5_3_D20250710_helper -description: > - OneSIG help center grouped tool. Use the `action` parameter to access help - documentation, product news red dots, version information and feedback - endpoints under the `/helper/*` routes. -description_cn: > - OneSIG 帮助中心分组工具。通过 `action` 调用帮助文档、产品动态红点、版本信息以及 - 问题反馈接口,对应 Web 控制台 `/helper/*` 子页。 -category: custom -enabled: true -requires_confirmation: false -provider: onesig_v2_5_3_D20250710_api -inputSchema: - type: object - properties: - action: - type: string - description: | - Helper 分组动作名: - - document_list POST /v3/document/list 分页查询帮助文档 - - document_preview GET /v3/document/preview 预览帮助文档(需 fileName) - - product_news_get GET /v3/product/news 产品动态红点 - - product_news_mark_read PUT /v3/product/news 标记产品动态已读 - - product_version GET /v3/product/version 产品版本信息 - - product_issue POST /v3/product/issue 提交产品问题反馈 - enum: - - document_list - - document_preview - - product_news_get - - product_news_mark_read - - product_version - - product_issue - fileName: - type: string - description: 帮助文档文件名(document_preview 必填) - pageNo: - type: integer - default: 1 - pageSize: - type: integer - default: 20 - search: - type: string - description: 文档列表模糊搜索关键字 - documentUpdate: - type: boolean - description: 标记帮助文档红点已读 - versionUpdate: - type: boolean - description: 标记版本更新红点已读 - required: - - action -handler: - type: script - script_file: onesig_v2_5_3_D20250710.handler.py - function: helper diff --git a/.flocks/flockshub/plugins/tools/api/onesig_v2_5_3_D20250710/onesig_v2_5_3_D20250710_login.yaml b/.flocks/flockshub/plugins/tools/api/onesig_v2_5_3_D20250710/onesig_v2_5_3_D20250710_login.yaml deleted file mode 100644 index a73f9b618..000000000 --- a/.flocks/flockshub/plugins/tools/api/onesig_v2_5_3_D20250710/onesig_v2_5_3_D20250710_login.yaml +++ /dev/null @@ -1,134 +0,0 @@ -name: onesig_v2_5_3_D20250710_login -description: > - OneSIG (older v2.5) login / session management grouped tool. Same surface - as the standard `onesig_login`, but the login endpoint receives the - **plaintext** password directly (no `GET /v3/pubkey`, no RSA-OAEP on - `POST /v3/login`). Use the `action` parameter to log in (build a cookie - session), log out, change the current user's password, pull captcha / - pubkey / account info, and manage product news / TOTP recovery codes. - Most other `onesig_v2_5_3_D20250710_*` tools auto-login on demand so explicit - `login` calls are only needed when captcha or TOTP is required. - - Auth-flow endpoints handled internally (NOT registered as ActionSpecs): - - GET /v3/captcha fetched implicitly during login - - POST /v3/login driven by `login` action — body carries - `username` + **plaintext** `password` - - POST /v3/login/totp driven by `login` action when responseCode 1012 - or when the device requires inline TOTP - - POST /v3/logout driven by `logout` action - - GET /v3/pubkey still fetched *only* before sensitive *non-login* - write fields (change_password, user_create, …) - - PUT /v3/user/password driven by `change_password` action (each of - oldPassword / newPassword / dupPassword is - auto RSA-OAEP encrypted with the latest pubkey) -description_cn: > - OneSIG(v2.5 老版本)登录/会话管理工具。接口形态与标准 `onesig_login` 完全 - 一致,区别仅在于登录链路:本插件 `POST /v3/login` 直接发送**明文**密码, - 不再调 `GET /v3/pubkey` 也不做 RSA-OAEP 加密。 - 通过 `action` 调用登录、登出、改密、拉取验证码 / 账户信息,以及产品动态、 - TOTP 恢复码等接口。其他 `onesig_v2_5_3_D20250710_*` 工具会按需自动登录,仅在设备 - 启用图形验证码或 TOTP 时需要显式调用 `login` 并传入 `captcha`/`totp`。 - - 专用鉴权流程(已在处理器内部封装,不以 ActionSpec 形式开放): - - GET /v3/captcha 登录时自动拉取,判断是否需 captcha / inline TOTP - - POST /v3/login 由 `login` 动作驱动;body 形如 - `{username, password=<明文>, captcha?, checksum?}` - - POST /v3/login/totp `login` 动作在 responseCode 1012 或 inline-TOTP - 场景下自动续走 - - POST /v3/logout 由 `logout` 动作驱动 - - GET /v3/pubkey 仅在 *非登录* 的敏感写字段(改密 / 删用户 / - 删审计 / 接口启停 / 升级口令)发送前才拉取 - - PUT /v3/user/password 由 `change_password` 驱动;oldPassword / - newPassword / dupPassword 三个字段会用最新公钥 - 分别做 RSA-OAEP 加密(无需调用方手动加密) -category: custom -enabled: true -requires_confirmation: true -provider: onesig_v2_5_3_D20250710_api -inputSchema: - type: object - properties: - action: - type: string - description: | - Login 分组动作名,可选值: - - login - 用途: 显式触发登录流程(拉 captcha → POST /v3/login 携带**明文**密码,必要时再走 /v3/login/totp) - 必填: 无(用户名密码取自服务凭据) - 常用: `captcha`、`totp` - 风险提示: 大多数业务调用会按需自动登录,仅在启用验证码 / TOTP 时需手动登录; - 注意本插件**登录阶段以明文形式下发密码**,建议仅在内网受控网络 + HTTPS + verify_ssl=true 下使用 - - logout - 用途: 退出当前会话(POST /v3/logout)并清空本地 Cookie - 必填: 无 - 常用: 无 - 风险提示: 后续调用会重新登录 - - change_password - 用途: 修改当前用户密码(PUT /v3/user/password,三个密码字段会自动 RSA-OAEP 加密) - 必填: `new_password` - 常用: `old_password`、`dup_password`、`username` - 风险提示: 写操作;密码长度需 8~20 且含字母+数字+特殊字符 - - get_captcha - 用途: 获取图形验证码状态(GET /v3/captcha) - 必填: 无 - - get_pubkey - 用途: 获取登录 RSA 公钥(GET /v3/pubkey) - 必填: 无 - - get_account - 用途: 获取当前账户信息(GET /v3/account) - 必填: 无 - - get_recovery_code - 用途: 查看 TOTP 恢复码(GET /v3/user/recoveryCode) - 必填: 无 - - regenerate_recovery_code - 用途: 重新生成 TOTP 恢复码(PUT /v3/user/recoveryCode) - 必填: 无 - 风险提示: 旧恢复码立即失效 - - get_product_news - 用途: 获取产品动态红点状态(GET /v3/product/news) - 必填: 无 - - mark_product_news_read - 用途: 标记产品动态已读(PUT /v3/product/news) - 必填: 至少一个布尔字段(如 `documentUpdate=false`) - 风险提示: 写操作 - enum: - - login - - logout - - change_password - - get_captcha - - get_pubkey - - get_account - - get_recovery_code - - regenerate_recovery_code - - get_product_news - - mark_product_news_read - captcha: - type: string - description: 当设备启用图形验证码时填写(4 位)。仅在 `login` 动作有效。 - totp: - type: string - description: 当设备启用 TOTP(双因素)时填写动态口令或恢复码。仅在 `login` 动作有效。 - username: - type: string - description: 改密时使用的用户名,默认取服务凭据中的 `username`。 - new_password: - type: string - description: 新密码明文,长度 8~20,需包含字母、数字与特殊字符。仅在 `change_password` 动作必填。 - old_password: - type: string - description: 当前密码明文。强制改密分支可省略;常规改密必填。 - dup_password: - type: string - description: 新密码确认明文,留空时与 `new_password` 相同。 - documentUpdate: - type: boolean - description: 标记帮助文档红点是否已读(mark_product_news_read 使用)。 - versionUpdate: - type: boolean - description: 标记版本更新红点是否已读(mark_product_news_read 使用)。 - required: - - action -handler: - type: script - script_file: onesig_v2_5_3_D20250710.handler.py - function: login diff --git a/.flocks/flockshub/plugins/tools/api/onesig_v2_5_3_D20250710/onesig_v2_5_3_D20250710_monitoring.yaml b/.flocks/flockshub/plugins/tools/api/onesig_v2_5_3_D20250710/onesig_v2_5_3_D20250710_monitoring.yaml deleted file mode 100644 index c92802b0f..000000000 --- a/.flocks/flockshub/plugins/tools/api/onesig_v2_5_3_D20250710/onesig_v2_5_3_D20250710_monitoring.yaml +++ /dev/null @@ -1,256 +0,0 @@ -name: onesig_v2_5_3_D20250710_monitoring -description: > - OneSIG monitoring grouped tool. Use the `action` parameter to access - dashboard, threat overview, device status, alert host, inbound / outbound - threat events, and report management APIs. The handler accepts arbitrary - filter / pagination keys and forwards them to the JSON body, matching the - Web console behaviour. -description_cn: > - OneSIG 监控分组工具。通过 `action` 调用仪表盘、威胁防护大屏、设备状态、失陷主机、 - 入站 / 出站威胁事件、报告管理等接口。处理器会把所有过滤 / 分页字段透传到 JSON - 请求体,与 Web 控制台调用方式保持一致。 -category: custom -enabled: true -requires_confirmation: true -provider: onesig_v2_5_3_D20250710_api -inputSchema: - type: object - properties: - action: - type: string - description: | - Monitoring 分组动作名,按页面分类: - - # 仪表盘 (/monitoring/dashboard) - - dashboard_overview POST /v3/dashboard/overview 仪表盘总览数据 - - dashboard_outbound POST /v3/dashboard/outbound 仪表盘出站数据 - - dashboard_inbound POST /v3/dashboard/inbound 仪表盘入站数据 - - dashboard_zeroday POST /v3/dashboard/zeroday 仪表盘零日数据 - - dashboard_status GET /v3/dashboard/status 仪表盘服务状态 - - dashboard_ioc_type_sum GET /v3/dashboard/ioctypesum IOC 类型汇总 - - set_custom_config PUT /v3/setting/customConfig 更新仪表盘自定义配置(写) - - # 威胁防护大屏 (/monitoring/overview) - - common_threat_type_list GET /v3/common/threatTypeList 威胁类型列表 - - overview_event_inbound POST /v3/overview/eventInbound 入站事件 - - overview_event_outbound POST /v3/overview/eventOutbound 出站事件 - - overview_export_event_inbound POST /v3/overview/exportEventInbound 导出入站事件(文件) - - overview_export_event_outbound POST /v3/overview/exportEventOutbound 导出出站事件(文件) - - overview_asset_brief POST /v3/overview/assetBrief 资产简报 - - overview_asset_top POST /v3/overview/assetTop 资产 TOP - - overview_event_inbound_agg POST /v3/overview/eventInboundAgg 入站聚合 - - overview_export_event_inbound_agg POST /v3/overview/exportEventInboundAgg 导出入站聚合(文件) - - overview_event_outbound_agg POST /v3/overview/eventOutboundAgg 出站聚合 - - overview_export_event_outbound_agg POST /v3/overview/exportEventOutboundAgg 导出出站聚合(文件) - - overview_event_recent_agg POST /v3/overview/eventRecentAgg 近期事件聚合 - - overview_event_trend POST /v3/overview/eventTrend 事件趋势 - - overview_traffic_trend POST /v3/overview/trafficTrend 流量趋势 - - overview_stat POST /v3/overview/stat 总览统计卡片 - - overview_threat_type_proportion POST /v3/overview/threatTypeProportion 威胁类型占比 - - overview_ioc_type_proportion POST /v3/overview/iocTypeProportion IOC 类型占比 - - overview_export_threat_type_proportion POST /v3/overview/exportThreatTypeProportion 导出威胁类型占比(文件) - - overview_export_ioc_type_proportion POST /v3/overview/exportIocTypeProportion 导出 IOC 类型占比(文件) - - get_overview_config GET /v3/setting/overviewConfig 总览展示配置 - - set_overview_config PUT /v3/setting/overviewConfig 更新总览展示配置(写) - - # 设备状态 (/monitoring/status) - - device_platform_status GET /v3/device/platformStatus 平台运行状态 - - device_system_status GET /v3/device/systemStatus 系统资源状态 - - device_network_status GET /v3/device/networkStatus 网络状态 - - common_interface_list GET /v3/common/interfaceList 网口列表 - - basic_cpu_attr GET /v3/basic/cpuAttr CPU 属性 - - # 失陷主机 (/monitoring/hosts, /monitoring/hostdetail) - - alert_host_stat GET /v3/alertHost/stat 失陷主机统计 - - alert_host_tree POST /v3/alertHost/tree 失陷主机资产树 - - alert_host_list POST /v3/alertHost/list 分页查询失陷主机(必填 startTime/endTime) - - alert_host_export POST /v3/alertHost/export 导出失陷主机(文件) - - alert_host_detail POST /v3/alertHost/detail 失陷主机详情概览 - - alert_host_detail_list POST /v3/alertHost/detail/list 失陷主机详情关联列表 - - alert_host_detail_export POST /v3/alertHost/detail/export 导出失陷主机详情(文件) - - common_asset_type_list GET /v3/common/assetTypeList 资产类型列表 - - # 入站威胁 (/monitoring/inbound_threat) - - event_inbound_stat GET /v3/event/inbound/stat 入站威胁统计 - - event_inbound_list POST /v3/event/inbound/list 分页查询入站威胁(必填 startTime/endTime) - - event_inbound_export POST /v3/event/inbound/export 导出入站威胁(文件) - - event_inbound_detail POST /v3/event/inbound/detail 入站威胁详情 - - event_inbound_detail_trend POST /v3/event/inbound/detail/trend 入站威胁详情趋势 - - event_inbound_detail_list POST /v3/event/inbound/detail/list 入站威胁详情列表 - - event_inbound_detail_export POST /v3/event/inbound/detail/export 导出入站威胁详情(文件) - - port_protect_group_list POST /v3/portProtectGroup/list 端口防护组列表(共享) - - web_custom_column_set PUT /v3/webCustomColumn/set 保存自定义列配置(写) - - # 出站威胁 (/monitoring/outbound_threat) - - event_outbound_stat GET /v3/event/outbound/stat 出站威胁统计 - - event_outbound_list POST /v3/event/outbound/list 分页查询出站威胁(必填 startTime/endTime) - - event_outbound_export POST /v3/event/outbound/export 导出出站威胁(文件) - - event_outbound_detail POST /v3/event/outbound/detail 出站威胁详情 - - event_outbound_detail_trend POST /v3/event/outbound/detail/trend 出站威胁详情趋势 - - event_outbound_detail_list POST /v3/event/outbound/detail/list 出站威胁详情列表 - - event_outbound_detail_export POST /v3/event/outbound/detail/export 导出出站威胁详情(文件) - - set_dnslog_config PUT /v3/setting/dnslogConfig 更新 DNS 日志接入配置(写) - - # 报告管理 (/monitoring/report) - - get_notice_config GET /v3/setting/noticeConfig 通知配置 - - report_form_create POST /v3/report/form 新增报表表单 - - report_form_list POST /v3/report/form/list 分页查询报表表单 - - report_form_download GET /v3/report/form/download 下载报表(文件,需 uniqueId) - - report_form_delete DELETE /v3/report/form 删除报表表单 - - report_task_list POST /v3/report/task/list 分页查询报表任务 - - report_task_create POST /v3/report/task 新增报表任务 - - report_task_update PUT /v3/report/task 更新报表任务 - - report_task_delete DELETE /v3/report/task 删除报表任务 - - report_task_test POST /v3/report/task/test 测试报表任务 - - # 共享 (monitoring layout) - - common_asset_group_tree GET /v3/common/assetGroupTree 资产组树 - - ips_rule_create POST /v3/ips/rule 新增 IPS 自定义规则 - - ips_rule_apply POST /v3/ips/rule/apply 应用 IPS 规则变更 - - ips_ruleset_namelist POST /v3/ips/ruleset/namelist 规则集名称列表 - - ips_ruleset_referred POST /v3/ips/ruleset/referred 规则集引用关系 - - logaccess_stat GET /v3/logAccess/stat 日志接入统计 - - get_dnslog_config GET /v3/setting/dnslogConfig DNS 日志接入配置 - enum: - - dashboard_overview - - dashboard_outbound - - dashboard_inbound - - dashboard_zeroday - - dashboard_status - - dashboard_ioc_type_sum - - set_custom_config - - common_threat_type_list - - overview_event_inbound - - overview_event_outbound - - overview_export_event_inbound - - overview_export_event_outbound - - overview_asset_brief - - overview_asset_top - - overview_event_inbound_agg - - overview_export_event_inbound_agg - - overview_event_outbound_agg - - overview_export_event_outbound_agg - - overview_event_recent_agg - - overview_event_trend - - overview_traffic_trend - - overview_stat - - overview_threat_type_proportion - - overview_ioc_type_proportion - - overview_export_threat_type_proportion - - overview_export_ioc_type_proportion - - get_overview_config - - set_overview_config - - device_platform_status - - device_system_status - - device_network_status - - common_interface_list - - basic_cpu_attr - - alert_host_stat - - alert_host_tree - - alert_host_list - - alert_host_export - - alert_host_detail - - alert_host_detail_list - - alert_host_detail_export - - common_asset_type_list - - event_inbound_stat - - event_inbound_list - - event_inbound_export - - event_inbound_detail - - event_inbound_detail_trend - - event_inbound_detail_list - - event_inbound_detail_export - - port_protect_group_list - - web_custom_column_set - - event_outbound_stat - - event_outbound_list - - event_outbound_export - - event_outbound_detail - - event_outbound_detail_trend - - event_outbound_detail_list - - event_outbound_detail_export - - set_dnslog_config - - get_notice_config - - report_form_create - - report_form_list - - report_form_download - - report_form_delete - - report_task_list - - report_task_create - - report_task_update - - report_task_delete - - report_task_test - - common_asset_group_tree - - ips_rule_create - - ips_rule_apply - - ips_ruleset_namelist - - ips_ruleset_referred - - logaccess_stat - - get_dnslog_config - startTime: - type: integer - description: 起始时间戳(Unix 秒) - endTime: - type: integer - description: 结束时间戳(Unix 秒) - pageNo: - type: integer - default: 1 - description: 分页页码 - pageSize: - type: integer - default: 20 - description: 分页大小 - sortBy: - type: string - description: 排序字段 - orderBy: - type: string - description: 排序方向,`asc` / `desc` - search: - type: string - description: 模糊搜索关键字 - severity: - type: array - items: - type: integer - description: 威胁等级过滤 - threatType: - type: array - items: - type: string - description: 威胁类型过滤 - assetType: - type: array - items: - type: string - description: 资产类型过滤 - threatLabel: - type: array - items: - type: string - description: 特定威胁标签过滤 - threatState: - type: string - description: 失陷主机状态过滤 - assetGroup: - type: string - description: 资产组路径,`全部` 转为空串 - direction: - type: string - description: 威胁方向(inbound/outbound) - isTls: - type: boolean - description: 是否仅 TLS 加密流量 - uniqueId: - type: string - description: 资源唯一 ID(删除/下载/详情等) - fileName: - type: string - description: 文件名(下载报表表单) - required: - - action -handler: - type: script - script_file: onesig_v2_5_3_D20250710.handler.py - function: monitoring diff --git a/.flocks/flockshub/plugins/tools/api/onesig_v2_5_3_D20250710/onesig_v2_5_3_D20250710_strategy.yaml b/.flocks/flockshub/plugins/tools/api/onesig_v2_5_3_D20250710/onesig_v2_5_3_D20250710_strategy.yaml deleted file mode 100644 index 54d423b42..000000000 --- a/.flocks/flockshub/plugins/tools/api/onesig_v2_5_3_D20250710/onesig_v2_5_3_D20250710_strategy.yaml +++ /dev/null @@ -1,268 +0,0 @@ -name: onesig_v2_5_3_D20250710_strategy -description: > - OneSIG strategy grouped tool. Use the `action` parameter to manage protection - policies including global whitelist / blacklist, multi-dimensional blocking, - API keys, syslog auto-blacklist, FTP / SFTP linkage, IPS rules and rule sets, - HTTP blacklist, port protection groups, and the strategy landing page itself. -description_cn: > - OneSIG 防护策略分组工具。通过 `action` 调用全局白/黑名单、多维封锁、API 联动密钥、 - Syslog 自动封禁、FTP/SFTP 联动、IPS 规则和规则集、HTTP 黑名单、端口防护组以及策略 - 首页相关接口。多数写操作具有破坏性,请谨慎调用。 -category: custom -enabled: true -requires_confirmation: true -provider: onesig_v2_5_3_D20250710_api -inputSchema: - type: object - properties: - action: - type: string - description: | - Strategy 分组动作名(按 UI 子页归类): - - # 白名单 (/strategy/whitelist) - - whitelist_add POST /v3/globalWhitelist 新增(多方向) - - whitelist_update PUT /v3/globalWhitelist 更新(编辑单方向) - - whitelist_delete DELETE /v3/globalWhitelist 删除(uniqueIds 数组) - - whitelist_export POST /v3/globalWhitelist/export 导出(文件) - - whitelist_import POST /v3/globalWhitelist/import 导入 - - whitelist_template GET /v3/globalWhitelist/template 导入模板(文件) - - whitelist_list POST /v3/globalWhitelist/list 分页查询 - - whitelist_remove_batch DELETE /v3/globalWhitelist/remove 批量移除 - - # 黑名单 (/strategy/blacklist) - - blacklist_location_options GET /v3/blacklist/location 地理位置选项 - - blacklist_add POST /v3/globalBlacklist 新增 - - blacklist_update PUT /v3/globalBlacklist 更新 - - blacklist_delete DELETE /v3/globalBlacklist 删除 - - blacklist_check POST /v3/globalBlacklist/check 冲突校验 - - blacklist_export POST /v3/globalBlacklist/export 导出(文件) - - blacklist_import POST /v3/globalBlacklist/import 导入 - - blacklist_template GET /v3/globalBlacklist/template 导入模板(文件) - - blacklist_list POST /v3/globalBlacklist/list 分页查询 - - blacklist_remove_batch DELETE /v3/globalBlacklist/remove 批量移除 - - # 多维封锁 (/strategy/multi_block, /strategy/add_multi_block) - - multiblock_executelog_list POST /v3/multiblock/executelog - - multiblock_executelog_export POST /v3/multiblock/executelog/export (文件) - - multiblock_rule_delete DELETE /v3/multiblock/rule - - multiblock_rule_active POST /v3/multiblock/rule/active - - multiblock_rule_dict POST /v3/multiblock/rule/dict - - multiblock_rule_list POST /v3/multiblock/rule/list - - multiblock_rule_get POST /v3/multiblock/rule/get - - multiblock_rule_preview POST /v3/multiblock/rule/preview - - multiblock_rule_create POST /v3/multiblock/rule - - multiblock_rule_update PUT /v3/multiblock/rule - - # API 联动 (/strategy/api) - - apikey_delete DELETE /v3/apikey - - apikey_update PUT /v3/apikey - - apikey_create POST /v3/apikey - - apikey_list GET /v3/apikey/list - - apikey_secret GET /v3/apikey/secret 需 uniqueId - - # Syslog 自动封禁 (/strategy/syslog) - - auto_blacklist_delete DELETE /v3/autoBlacklist - - auto_blacklist_check POST /v3/autoBlacklist/check - - auto_blacklist_create POST /v3/autoBlacklist - - auto_blacklist_update PUT /v3/autoBlacklist - - auto_blacklist_list POST /v3/autoBlacklist/list - - auto_blacklist_trend GET /v3/autoBlacklist/trend 需 startTime/endTime - - auto_blacklist_sample POST /v3/autoBlacklist/sample - - # FTP/SFTP 联动 (/strategy/ftp) - - linkage_delete DELETE /v3/linkage - - linkage_create POST /v3/linkage - - linkage_update PUT /v3/linkage - - linkage_enable POST /v3/linkage/enable - - linkage_info GET /v3/linkage/info 需 uniqueId - - linkage_list POST /v3/linkage/list - - linkage_template GET /v3/linkage/template (文件) - - linkage_test POST /v3/linkage/test - - # 入侵防护 (/strategy/intrusion_prevention) - - ips_rule_create POST /v3/ips/rule - - ips_rule_all POST /v3/ips/rule/all 全量操作 - - ips_rule_apply POST /v3/ips/rule/apply 应用变更 - - ips_rule_list POST /v3/ips/rule/list - - ips_ruleset_create POST /v3/ips/ruleset - - ips_ruleset_update PUT /v3/ips/ruleset - - ips_ruleset_delete DELETE /v3/ips/ruleset - - ips_ruleset_info POST /v3/ips/ruleset/info - - ips_ruleset_list POST /v3/ips/ruleset/list - - ips_ruleset_namelist POST /v3/ips/ruleset/namelist - - ips_threat_types POST /v3/ips/threatTypes - - # HTTP 防护 (/strategy/httpProtect) - - http_blacklist_delete DELETE /v3/httpBlacklist - - http_blacklist_enable POST /v3/httpBlacklist/enable - - http_blacklist_export POST /v3/httpBlacklist/export (文件) - - http_blacklist_list POST /v3/httpBlacklist/list - - http_blacklist_create POST /v3/httpBlacklist - - http_blacklist_update PUT /v3/httpBlacklist - - get_advanced_config GET /v3/setting/advancedConfig 共享高级配置 - - set_advanced_config PUT /v3/setting/advancedConfig 共享高级配置(写) - - get_xff_config GET /v3/setting/xffConfig - - set_xff_config PUT /v3/setting/xffConfig - - # 高危端口防护 (/strategy/portProtect) - - port_protect_group_delete DELETE /v3/portProtectGroup - - port_protect_group_create POST /v3/portProtectGroup - - port_protect_group_update PUT /v3/portProtectGroup - - port_protect_group_clone POST /v3/portProtectGroup/clone - - port_protect_group_default_info GET /v3/portProtectGroup/defaultInfo - - port_protect_group_list_full POST /v3/portProtectGroup/list - - port_protect_port_delete DELETE /v3/portProtectGroup/port - - port_protect_port_create POST /v3/portProtectGroup/port - - port_protect_port_update PUT /v3/portProtectGroup/port - - port_protect_port_export POST /v3/portProtectGroup/port/export (文件) - - port_protect_port_list POST /v3/portProtectGroup/port/list - - port_protect_port_onekey_import POST /v3/portProtectGroup/port/onekeyImport - - port_protect_port_onekey_status GET /v3/portProtectGroup/port/onekeyImport - - port_protect_portinfo POST /v3/portProtectGroup/portinfo - - # 策略首页 (/strategy/strategy) - - device_onekey_bypass POST /v3/device/onekeyBypass - - protection_policy_delete DELETE /v3/protection/policy - - protection_policy_update PUT /v3/protection/policy - - protection_policy_get GET /v3/protection/policy 需 uniqueId - - protection_policy_tree GET /v3/protection/policy/tree - - set_scan_config PUT /v3/setting/scanConfig - enum: - - whitelist_add - - whitelist_update - - whitelist_delete - - whitelist_export - - whitelist_import - - whitelist_template - - whitelist_list - - whitelist_remove_batch - - blacklist_location_options - - blacklist_add - - blacklist_update - - blacklist_delete - - blacklist_check - - blacklist_export - - blacklist_import - - blacklist_template - - blacklist_list - - blacklist_remove_batch - - multiblock_executelog_list - - multiblock_executelog_export - - multiblock_rule_delete - - multiblock_rule_active - - multiblock_rule_dict - - multiblock_rule_list - - multiblock_rule_get - - multiblock_rule_preview - - multiblock_rule_create - - multiblock_rule_update - - apikey_delete - - apikey_update - - apikey_create - - apikey_list - - apikey_secret - - auto_blacklist_delete - - auto_blacklist_check - - auto_blacklist_create - - auto_blacklist_update - - auto_blacklist_list - - auto_blacklist_trend - - auto_blacklist_sample - - linkage_delete - - linkage_create - - linkage_update - - linkage_enable - - linkage_info - - linkage_list - - linkage_template - - linkage_test - - ips_rule_create - - ips_rule_all - - ips_rule_apply - - ips_rule_list - - ips_ruleset_create - - ips_ruleset_update - - ips_ruleset_delete - - ips_ruleset_info - - ips_ruleset_list - - ips_ruleset_namelist - - ips_threat_types - - http_blacklist_delete - - http_blacklist_enable - - http_blacklist_export - - http_blacklist_list - - http_blacklist_create - - http_blacklist_update - - get_advanced_config - - set_advanced_config - - get_xff_config - - set_xff_config - - port_protect_group_delete - - port_protect_group_create - - port_protect_group_update - - port_protect_group_clone - - port_protect_group_default_info - - port_protect_group_list_full - - port_protect_port_delete - - port_protect_port_create - - port_protect_port_update - - port_protect_port_export - - port_protect_port_list - - port_protect_port_onekey_import - - port_protect_port_onekey_status - - port_protect_portinfo - - device_onekey_bypass - - protection_policy_delete - - protection_policy_update - - protection_policy_get - - protection_policy_tree - - set_scan_config - uniqueId: - type: string - description: 资源唯一 ID(GET / DELETE 单条目时使用) - uniqueIds: - type: array - items: - type: string - description: 批量删除使用的 ID 列表 - whiteList: - type: array - items: - type: object - description: 白名单方向数组(whitelist_add 必填,元素含 `direction`) - direction: - type: string - description: 白名单方向(whitelist_update 必填,可选 inbound/outbound/both) - condition: - type: array - items: - type: object - description: 白/黑名单条件列表 - comments: - type: string - description: 备注 - pageNo: - type: integer - default: 1 - pageSize: - type: integer - default: 20 - startTime: - type: integer - description: 起始时间戳(auto_blacklist_trend 等使用) - endTime: - type: integer - description: 结束时间戳 - name: - type: string - description: 名称(如 API 联动密钥名 / 端口防护组名) - enable: - type: boolean - description: 启停标识 - required: - - action -handler: - type: script - script_file: onesig_v2_5_3_D20250710.handler.py - function: strategy diff --git a/.flocks/flockshub/plugins/tools/api/sangfor_af_v8_0_48/_provider.yaml b/.flocks/flockshub/plugins/tools/api/sangfor_af_v8_0_48/_provider.yaml deleted file mode 100644 index e5dbcd188..000000000 --- a/.flocks/flockshub/plugins/tools/api/sangfor_af_v8_0_48/_provider.yaml +++ /dev/null @@ -1,55 +0,0 @@ -name: sangfor_af -service_id: sangfor_af -version: "8.0.48" -description: > - Sangfor AF (Application Firewall) v8.0.48 REST API service. - Uses session-based cookie authentication: call login to obtain - a token, then supply it via Cookie header in subsequent requests. -description_cn: > - 深信服 AF 下一代防火墙 v8.0.48 REST API 服务。 - 使用基于 Session 的 Cookie 认证:首先调用 login 获取 token, - 后续请求在 Cookie 中携带 token。基础 URL 为设备管理地址(如 https://192.168.1.1)。 -auth: - type: custom - secret: sangfor_af_v8_0_48_username - secret_secret: sangfor_af_v8_0_48_password -credential_fields: - - key: username - label: 管理员用户名 - storage: secret - config_key: username - secret_id: sangfor_af_v8_0_48_username - input_type: text - required: true - - key: password - label: 管理员密码 - storage: secret - config_key: password - secret_id: sangfor_af_v8_0_48_password - input_type: password - required: true - - key: base_url - label: 设备地址 (Base URL) - storage: config - config_key: base_url - input_type: url - default: "https://192.168.1.1" -defaults: - base_url: "https://192.168.1.1" - timeout: 60 - category: custom - product_version: "8.0.48" -notes: | - 深信服 AF v8.0.48 API 认证流程: - 1. 在 AF WebUI「系统 → 管理员账号」勾选 WEBAPI 权限。 - 2. Handler 自动调用 POST /api/v1/namespaces/public/login, - 用配置的用户名/密码换取 token 并缓存(带 keepalive 自动续期)。 - 3. 后续所有请求由 Handler 自动注入 Cookie: token=。 - 4. token 默认 10 分钟无操作后失效,缓存命中失败时会自动重新登录。 - - `verify_ssl` 由表单底部「SSL 验证」开关控制,下列字段都会被 - 识别为 SSL 验证开关,按以下优先级取值: - 1. `verify_ssl` (主键) - 2. `ssl_verify` (兼容别名) - 3. `custom_settings.verify_ssl` (WebUI 表单写入位置) - 4. 兜底默认值 `False`(**默认关闭证书验证**,AF 设备通常使用自签名证书) diff --git a/.flocks/flockshub/plugins/tools/api/sangfor_af_v8_0_48/_test.yaml b/.flocks/flockshub/plugins/tools/api/sangfor_af_v8_0_48/_test.yaml deleted file mode 100644 index 18f46895e..000000000 --- a/.flocks/flockshub/plugins/tools/api/sangfor_af_v8_0_48/_test.yaml +++ /dev/null @@ -1,103 +0,0 @@ -schema_version: 1 -provider: sangfor_af - -# Service-level connectivity probe. -# get_system_version is a lightweight read-only GET that verifies -# authentication and basic reachability. -connectivity: - tool: sangfor_af_v48_status - params: - action: get_system_version - -# Tool-level test samples shown in the WebUI ToolDetailDrawer drop-down. -fixtures: - sangfor_af_v48_auth: - - label: "Session keepalive" - label_cn: "刷新 Session 保活" - tags: [smoke] - params: - action: keepalive - assert: - success: true - - sangfor_af_v48_status: - - label: "Get system version" - label_cn: "获取系统版本信息" - tags: [smoke] - params: - action: get_system_version - assert: - success: true - - - label: "Get CPU usage" - label_cn: "获取 CPU 使用率" - tags: [smoke] - params: - action: get_cpu_usage - - - label: "Get memory usage" - label_cn: "获取内存使用率" - tags: [smoke] - params: - action: get_memory_usage - - sangfor_af_v48_ops: - - label: "List blacklist entries" - label_cn: "查询黑名单列表" - tags: [smoke, blacklist] - params: - action: get_blackwhitelist - type: BLACK - assert: - success: true - - - label: "List whitelist entries" - label_cn: "查询白名单列表" - tags: [smoke, whitelist] - params: - action: get_blackwhitelist - type: WHITE - - - label: "List blocked attacker IPs" - label_cn: "查询封锁攻击者 IP 列表" - tags: [smoke, blockip] - params: - action: get_blockip_list - assert: - success: true - - sangfor_af_v48_objects: - - label: "List IP groups" - label_cn: "查询 IP 地址组列表" - tags: [smoke] - params: - action: get_ipgroups - assert: - success: true - - - label: "List services" - label_cn: "查询服务对象列表" - tags: [smoke] - params: - action: get_services - assert: - success: true - - sangfor_af_v48_network: - - label: "List routing table (all routes)" - label_cn: "查询完整路由表" - tags: [smoke] - params: - action: get_routes - routeType: ALL_ROUTE - assert: - success: true - - sangfor_af_v48_system: - - label: "List admin accounts" - label_cn: "查询管理员账户列表" - tags: [smoke] - params: - action: get_accounts - assert: - success: true diff --git a/.flocks/flockshub/plugins/tools/api/sangfor_af_v8_0_48/sangfor_af.handler.py b/.flocks/flockshub/plugins/tools/api/sangfor_af_v8_0_48/sangfor_af.handler.py deleted file mode 100644 index d6c01d890..000000000 --- a/.flocks/flockshub/plugins/tools/api/sangfor_af_v8_0_48/sangfor_af.handler.py +++ /dev/null @@ -1,689 +0,0 @@ -""" -Sangfor AF (Application Firewall) v8.0.48 API Handler. - -Authentication: - - Session-based: POST /api/v1/namespaces/public/login → token - - All subsequent requests: Cookie: token= - - Token expires after ~10 min of inactivity (keepalive resets timer) - -API base URL: https:// -Namespace: /api/v1/namespaces/public/ -Batch ops: /api/batch/v1/namespaces/public/ -""" -from __future__ import annotations - -import os -from typing import Any, Callable, Optional - -import aiohttp - -from flocks.config.config_writer import ConfigWriter -from flocks.tool.registry import ToolContext, ToolResult - -# ── Constants ──────────────────────────────────────────────────────────────── - -SERVICE_ID = "sangfor_af_v8_0_48" -DEFAULT_BASE_URL = "https://192.168.1.1" -DEFAULT_TIMEOUT = 60 -NAMESPACE = "public" - -API_V1 = f"/api/v1/namespaces/{NAMESPACE}" -API_BATCH = f"/api/batch/v1/namespaces/{NAMESPACE}" - -# In-process token cache: {base_url: token} -_TOKEN_CACHE: dict[str, str] = {} - - -# ── Secret / Config helpers ─────────────────────────────────────────────────── - -def _get_secret_manager(): - from flocks.security import get_secret_manager - return get_secret_manager() - - -def _resolve_ref(value: Any) -> Optional[str]: - if value is None: - return None - if not isinstance(value, str): - return str(value) - if value.startswith("{secret:") and value.endswith("}"): - return _get_secret_manager().get(value[len("{secret:"): -1]) - if value.startswith("{env:") and value.endswith("}"): - return os.getenv(value[len("{env:"): -1]) - return value - - -def _service_config() -> dict[str, Any]: - raw = ConfigWriter.get_api_service_raw(SERVICE_ID) - return raw if isinstance(raw, dict) else {} - - -def _resolve_verify_ssl(raw: dict[str, Any]) -> bool: - """Read verify_ssl with the same priority as sangfor_sip / onesec: - verify_ssl > ssl_verify > custom_settings.verify_ssl > False. - AF devices commonly use self-signed certs, so default is False. - """ - value = raw.get("verify_ssl") - if value is None: - value = raw.get("ssl_verify") - if value is None: - custom = raw.get("custom_settings") - if isinstance(custom, dict): - value = custom.get("verify_ssl") - if isinstance(value, bool): - return value - if isinstance(value, str): - return value.strip().lower() in {"1", "true", "yes", "on"} - return False - - -def _resolve_runtime_config() -> tuple[str, int, str, str, bool]: - """Returns (base_url, timeout, username, password, verify_ssl).""" - raw = _service_config() - base_url = ( - _resolve_ref(raw.get("base_url")) or DEFAULT_BASE_URL - ).rstrip("/") - timeout = raw.get("timeout", DEFAULT_TIMEOUT) - try: - timeout = int(timeout) - except (TypeError, ValueError): - timeout = DEFAULT_TIMEOUT - - sm = _get_secret_manager() - - username = ( - _resolve_ref(raw.get("username")) - or sm.get("sangfor_af_v8_0_48_username") - or os.getenv("AF_USERNAME") - ) - password = ( - _resolve_ref(raw.get("password")) - or sm.get("sangfor_af_v8_0_48_password") - or os.getenv("AF_PASSWORD") - ) - - if not username or not password: - raise ValueError( - "AF API credentials not configured. " - "Please set username and password in the service configuration." - ) - return base_url, timeout, username, password, _resolve_verify_ssl(raw) - - -# ── Session / Token management ──────────────────────────────────────────────── - -async def _login( - session: aiohttp.ClientSession, - base_url: str, - username: str, - password: str, - verify_ssl: bool, -) -> tuple[Optional[str], Optional[str]]: - """Login and return (token, error_message).""" - url = f"{base_url}{API_V1}/login" - try: - async with session.post( - url, - json={"name": username, "password": password}, - ssl=verify_ssl, - ) as resp: - data = await resp.json(content_type=None) - except aiohttp.ClientError as exc: - return None, f"AF login request failed: {exc}" - - code = data.get("code") - if code != 0: - msg = data.get("message", "Unknown error") - return None, f"AF login failed (code={code}): {msg}" - - token = ( - data.get("data", {}).get("loginResult", {}).get("token") - ) - if not token: - return None, "AF login succeeded but no token returned" - return token, None - - -async def _get_token( - session: aiohttp.ClientSession, - base_url: str, - username: str, - password: str, - verify_ssl: bool, -) -> tuple[Optional[str], Optional[str]]: - """Return cached token or obtain a new one.""" - cached = _TOKEN_CACHE.get(base_url) - if cached: - # Validate by keepalive - try: - async with session.get( - f"{base_url}{API_V1}/keepalive", - headers={"Cookie": f"token={cached}"}, - ssl=verify_ssl, - ) as resp: - ka_data = await resp.json(content_type=None) - if ka_data.get("code") == 0: - return cached, None - except Exception: - pass - - token, err = await _login(session, base_url, username, password, verify_ssl) - if err: - return None, err - _TOKEN_CACHE[base_url] = token - return token, None - - -# ── Low-level HTTP ──────────────────────────────────────────────────────────── - -def _pick(params: dict[str, Any], *keys: str) -> dict[str, Any]: - return {k: params[k] for k in keys if k in params and params[k] is not None} - - -def _af_result(action: str, payload: Any) -> ToolResult: - metadata = {"source": "Sangfor AF", "api": action, "version": "8.0.48"} - if isinstance(payload, dict): - code = payload.get("code") - if code not in (None, 0): - msg = payload.get("message", "Unknown error") - return ToolResult( - success=False, - error=f"AF API error (code={code}): {msg}", - metadata=metadata, - ) - return ToolResult( - success=True, - output=payload.get("data", payload), - metadata=metadata, - ) - return ToolResult(success=True, output=payload, metadata=metadata) - - -async def _call( - method: str, - path: str, - params: Optional[dict[str, Any]] = None, - json: Optional[Any] = None, - action: str = "", -) -> ToolResult: - """Execute an authenticated AF API request.""" - try: - base_url, timeout, username, password, verify_ssl = _resolve_runtime_config() - except ValueError as exc: - return ToolResult(success=False, error=str(exc)) - - headers = {"Content-Type": "application/json"} - - async with aiohttp.ClientSession( - timeout=aiohttp.ClientTimeout(total=timeout) - ) as session: - token, err = await _get_token(session, base_url, username, password, verify_ssl) - if err: - return ToolResult(success=False, error=err) - - headers["Cookie"] = f"token={token}" - url = f"{base_url}{path}" - - try: - async with session.request( - method.upper(), - url, - params=params, - json=json, - headers=headers, - ssl=verify_ssl, - ) as resp: - if resp.status >= 400: - text = await resp.text() - return ToolResult( - success=False, - error=f"HTTP {resp.status}: {text[:500]}", - ) - data = await resp.json(content_type=None) - except aiohttp.ClientError as exc: - return ToolResult(success=False, error=f"Request failed: {exc}") - except Exception as exc: - return ToolResult(success=False, error=f"Unexpected error: {exc}") - - return _af_result(action or path.rsplit("/", 1)[-1], data) - - -# ── Action specs ───────────────────────────────────────────────────────────── - -class ActionSpec: - def __init__( - self, - method: str, - path_template: str, - param_builder: Callable[[dict[str, Any]], tuple[ - Optional[dict], Optional[Any] - ]], - required: tuple[str, ...] = (), - ) -> None: - self.method = method - self.path_template = path_template - self.param_builder = param_builder - self.required = required - - def build_path(self, params: dict[str, Any]) -> str: - try: - return self.path_template.format(**params) - except KeyError: - return self.path_template - - -# ── Auth actions ────────────────────────────────────────────────────────────── - -async def _do_login(ctx: ToolContext, **params: Any) -> ToolResult: - """Explicitly login and refresh the cached token.""" - del ctx - try: - base_url, timeout, username, password, verify_ssl = _resolve_runtime_config() - except ValueError as exc: - return ToolResult(success=False, error=str(exc)) - - async with aiohttp.ClientSession( - timeout=aiohttp.ClientTimeout(total=timeout) - ) as session: - token, err = await _login(session, base_url, username, password, verify_ssl) - if err: - return ToolResult(success=False, error=err) - _TOKEN_CACHE[base_url] = token - return ToolResult( - success=True, - output={"token": token, "message": "Login successful"}, - metadata={"source": "Sangfor AF", "api": "login", "version": "8.0.48"}, - ) - - -async def _do_logout(ctx: ToolContext, **params: Any) -> ToolResult: - del ctx - result = await _call("POST", f"{API_V1}/logout", action="logout") - try: - base_url, *_ = _resolve_runtime_config() - _TOKEN_CACHE.pop(base_url, None) - except ValueError: - pass - return result - - -async def _do_keepalive(ctx: ToolContext, **params: Any) -> ToolResult: - del ctx - return await _call("GET", f"{API_V1}/keepalive", action="keepalive") - - -# ── Objects actions ────────────────────────────────────────────────────────── - -async def _do_get_ipgroups(ctx: ToolContext, **params: Any) -> ToolResult: - del ctx - query = _pick( - params, - "_start", "_length", "businessType", "__nameprefix", "important", - "_search", "_order", "_sortby", "addressType", - ) - return await _call("GET", f"{API_V1}/ipgroups", params=query, action="get_ipgroups") - - -async def _do_get_ipgroup(ctx: ToolContext, **params: Any) -> ToolResult: - del ctx - uuid = params.get("uuid", "") - return await _call("GET", f"{API_V1}/ipgroups/{uuid}", action="get_ipgroup") - - -async def _do_create_ipgroup(ctx: ToolContext, **params: Any) -> ToolResult: - del ctx - body = _pick( - params, - "name", "businessType", "description", "addressType", "important", - "ipRanges", "creator", - ) - return await _call("POST", f"{API_V1}/ipgroups", json={"obj": body}, action="create_ipgroup") - - -async def _do_update_ipgroup(ctx: ToolContext, **params: Any) -> ToolResult: - del ctx - uuid = params.get("uuid", "") - body = _pick( - params, - "name", "businessType", "description", "addressType", "important", - "ipRanges", "creator", - ) - return await _call("PATCH", f"{API_V1}/ipgroups/{uuid}", json={"obj": body}, action="update_ipgroup") - - -async def _do_delete_ipgroup(ctx: ToolContext, **params: Any) -> ToolResult: - del ctx - uuid = params.get("uuid", "") - return await _call("DELETE", f"{API_V1}/ipgroups/{uuid}", action="delete_ipgroup") - - -async def _do_get_services(ctx: ToolContext, **params: Any) -> ToolResult: - del ctx - query = _pick(params, "_start", "_length", "_search", "_order", "_sortby", "serviceType") - return await _call("GET", f"{API_V1}/services", params=query, action="get_services") - - -async def _do_get_service(ctx: ToolContext, **params: Any) -> ToolResult: - del ctx - uuid = params.get("uuid", "") - return await _call("GET", f"{API_V1}/services/{uuid}", action="get_service") - - -# ── Operations center actions ───────────────────────────────────────────────── - -async def _do_get_blackwhitelist(ctx: ToolContext, **params: Any) -> ToolResult: - del ctx - query = _pick(params, "type", "_start", "_length", "_search", "_order", "description") - return await _call("GET", f"{API_V1}/whiteblacklist", params=query, action="get_blackwhitelist") - - -async def _do_add_blackwhitelist(ctx: ToolContext, **params: Any) -> ToolResult: - del ctx - body = _pick(params, "url", "type", "enable", "description", "domain") - return await _call("POST", f"{API_V1}/whiteblacklist", json={"obj": body}, action="add_blackwhitelist") - - -async def _do_batch_add_blackwhitelist(ctx: ToolContext, **params: Any) -> ToolResult: - del ctx - items = params.get("items", []) - return await _call( - "POST", - f"{API_BATCH}/whiteblacklist", - json=items, - action="batch_add_blackwhitelist", - ) - - -async def _do_delete_blackwhitelist(ctx: ToolContext, **params: Any) -> ToolResult: - del ctx - url_param = params.get("url", "") - list_type = params.get("type", "") - query = {"type": list_type} if list_type else None - return await _call( - "DELETE", - f"{API_V1}/whiteblacklist/{url_param}", - params=query, - action="delete_blackwhitelist", - ) - - -async def _do_batch_delete_blackwhitelist(ctx: ToolContext, **params: Any) -> ToolResult: - del ctx - items = params.get("items", []) - return await _call( - "POST", - f"{API_BATCH}/whiteblacklist", - params={"_method": "DELETE"}, - json=items, - action="batch_delete_blackwhitelist", - ) - - -async def _do_get_blockip_list(ctx: ToolContext, **params: Any) -> ToolResult: - del ctx - query = _pick(params, "_start", "_length", "_sortby", "_order", "creator", "fuzzyIP") - return await _call("GET", f"{API_V1}/blockip", params=query, action="get_blockip_list") - - -async def _do_batch_add_blockip(ctx: ToolContext, **params: Any) -> ToolResult: - del ctx - items = params.get("items", []) - query = _pick(params, "aifwType") - return await _call( - "POST", - f"{API_BATCH}/blockip", - params=query or None, - json=items, - action="batch_add_blockip", - ) - - -async def _do_batch_delete_blockip(ctx: ToolContext, **params: Any) -> ToolResult: - del ctx - items = params.get("items", []) - return await _call( - "POST", - f"{API_BATCH}/blockip", - params={"_method": "DELETE"}, - json=items, - action="batch_delete_blockip", - ) - - -async def _do_clear_blockip(ctx: ToolContext, **params: Any) -> ToolResult: - del ctx - query = _pick(params, "creator") - return await _call( - "DELETE", - f"{API_V1}/blockip", - params=query or None, - action="clear_blockip", - ) - - -async def _do_get_blockip_auto_config(ctx: ToolContext, **params: Any) -> ToolResult: - del ctx - return await _call("GET", f"{API_V1}/blockip/autoconfig", action="get_blockip_auto_config") - - -async def _do_set_blockip_auto_config(ctx: ToolContext, **params: Any) -> ToolResult: - del ctx - body = _pick(params, "blockTime") - return await _call( - "PUT", - f"{API_V1}/blockip/autoconfig", - json={"obj": body}, - action="set_blockip_auto_config", - ) - - -# ── Status / device info actions ────────────────────────────────────────────── - -async def _do_get_memory_usage(ctx: ToolContext, **params: Any) -> ToolResult: - del ctx - return await _call("GET", f"{API_V1}/memoryusage", action="get_memory_usage") - - -async def _do_get_cpu_usage(ctx: ToolContext, **params: Any) -> ToolResult: - del ctx - return await _call("GET", f"{API_V1}/cpuusage", action="get_cpu_usage") - - -async def _do_get_disk_usage(ctx: ToolContext, **params: Any) -> ToolResult: - del ctx - return await _call("GET", f"{API_V1}/diskusage", action="get_disk_usage") - - -async def _do_get_system_version(ctx: ToolContext, **params: Any) -> ToolResult: - del ctx - query = _pick(params, "filter") - return await _call( - "GET", f"{API_V1}/systemversion", - params=query or None, - action="get_system_version", - ) - - -async def _do_get_interface_status(ctx: ToolContext, **params: Any) -> ToolResult: - del ctx - # AF8.0.x: /interfacestatus returns 1002; use /interfaces (list) or - # /interfaces/status?interfaceName= (single interface query). - iface = params.get("interfaceNames") or params.get("interfaceName") or "" - if iface: - return await _call( - "GET", f"{API_V1}/interfaces/status", - params={"interfaceName": iface}, - action="get_interface_status", - ) - return await _call("GET", f"{API_V1}/interfaces", action="get_interface_status") - - -async def _do_get_runtime_status(ctx: ToolContext, **params: Any) -> ToolResult: - del ctx - return await _call("GET", f"{API_V1}/runtimestatus", action="get_runtime_status") - - -async def _do_get_current_time(ctx: ToolContext, **params: Any) -> ToolResult: - del ctx - return await _call("GET", f"{API_V1}/currenttime", action="get_current_time") - - -# ── Network / routing actions ───────────────────────────────────────────────── - -async def _do_get_routes(ctx: ToolContext, **params: Any) -> ToolResult: - del ctx - query = _pick(params, "_start", "_length", "routeType", "_search") - return await _call("GET", f"{API_V1}/routes", params=query or None, action="get_routes") - - -async def _do_get_routes_ipv6(ctx: ToolContext, **params: Any) -> ToolResult: - del ctx - query = _pick(params, "_start", "_length", "routeType", "_search") - return await _call("GET", f"{API_V1}/routes/ipv6", params=query or None, action="get_routes_ipv6") - - -# ── Admin account actions ───────────────────────────────────────────────────── - -async def _do_get_accounts(ctx: ToolContext, **params: Any) -> ToolResult: - del ctx - query = _pick(params, "_start", "_length", "_search", "enable") - return await _call("GET", f"{API_V1}/account", params=query or None, action="get_accounts") - - -async def _do_get_account(ctx: ToolContext, **params: Any) -> ToolResult: - del ctx - name = params.get("name", "") - return await _call("GET", f"{API_V1}/account/{name}", action="get_account") - - -# ── Action dispatch ─────────────────────────────────────────────────────────── - -_ACTION_MAP: dict[str, Callable] = { - # Auth - "login": _do_login, - "logout": _do_logout, - "keepalive": _do_keepalive, - # Objects - "get_ipgroups": _do_get_ipgroups, - "get_ipgroup": _do_get_ipgroup, - "create_ipgroup": _do_create_ipgroup, - "update_ipgroup": _do_update_ipgroup, - "delete_ipgroup": _do_delete_ipgroup, - "get_services": _do_get_services, - "get_service": _do_get_service, - # Operations center - blacklist/whitelist - "get_blackwhitelist": _do_get_blackwhitelist, - "add_blackwhitelist": _do_add_blackwhitelist, - "batch_add_blackwhitelist": _do_batch_add_blackwhitelist, - "delete_blackwhitelist": _do_delete_blackwhitelist, - "batch_delete_blackwhitelist": _do_batch_delete_blackwhitelist, - # Operations center - blocked IPs - "get_blockip_list": _do_get_blockip_list, - "batch_add_blockip": _do_batch_add_blockip, - "batch_delete_blockip": _do_batch_delete_blockip, - "clear_blockip": _do_clear_blockip, - "get_blockip_auto_config": _do_get_blockip_auto_config, - "set_blockip_auto_config": _do_set_blockip_auto_config, - # Status - "get_memory_usage": _do_get_memory_usage, - "get_cpu_usage": _do_get_cpu_usage, - "get_disk_usage": _do_get_disk_usage, - "get_system_version": _do_get_system_version, - "get_interface_status": _do_get_interface_status, - "get_runtime_status": _do_get_runtime_status, - "get_current_time": _do_get_current_time, - # Network - "get_routes": _do_get_routes, - "get_routes_ipv6": _do_get_routes_ipv6, - # System - "get_accounts": _do_get_accounts, - "get_account": _do_get_account, -} - -GROUP_ACTIONS: dict[str, set[str]] = { - "auth": {"login", "logout", "keepalive"}, - "objects": {"get_ipgroups", "get_ipgroup", "create_ipgroup", "update_ipgroup", "delete_ipgroup", "get_services", "get_service"}, - "ops": { - "get_blackwhitelist", "add_blackwhitelist", "batch_add_blackwhitelist", - "delete_blackwhitelist", "batch_delete_blackwhitelist", - "get_blockip_list", "batch_add_blockip", "batch_delete_blockip", - "clear_blockip", "get_blockip_auto_config", "set_blockip_auto_config", - }, - "status": { - "get_memory_usage", "get_cpu_usage", "get_disk_usage", - "get_system_version", "get_interface_status", - "get_runtime_status", "get_current_time", - }, - "network": {"get_routes", "get_routes_ipv6"}, - "system": {"get_accounts", "get_account"}, -} - -_CONNECTIVITY_TEST_ACTIONS: dict[str, str] = { - "auth": "keepalive", - "objects": "get_ipgroups", - "ops": "get_blackwhitelist", - "status": "get_system_version", - "network": "get_routes", - "system": "get_accounts", -} - - -async def unified_ops(ctx: ToolContext, action: str, **params: Any) -> ToolResult: - handler = _ACTION_MAP.get(action) - if handler is None: - available = ", ".join(sorted(_ACTION_MAP)) - return ToolResult( - success=False, - error=f"Unknown action: {action}. Available: {available}", - ) - return await handler(ctx, **params) - - -async def _dispatch_group(ctx: ToolContext, group: str, action: str, **params: Any) -> ToolResult: - if action == "test": - test_action = _CONNECTIVITY_TEST_ACTIONS.get(group, "get_system_version") - return await unified_ops(ctx, action=test_action, **params) - if action not in GROUP_ACTIONS[group]: - available = ", ".join(sorted(GROUP_ACTIONS[group])) - return ToolResult( - success=False, - error=f"Unsupported {group} action: {action}. Available: {available}", - ) - return await unified_ops(ctx, action=action, **params) - - -async def auth(ctx: ToolContext, action: str, **params: Any) -> ToolResult: - return await _dispatch_group(ctx, "auth", action, **params) - - -async def objects(ctx: ToolContext, action: str, **params: Any) -> ToolResult: - return await _dispatch_group(ctx, "objects", action, **params) - - -async def ops(ctx: ToolContext, action: str, **params: Any) -> ToolResult: - return await _dispatch_group(ctx, "ops", action, **params) - - -async def status(ctx: ToolContext, action: str, **params: Any) -> ToolResult: - return await _dispatch_group(ctx, "status", action, **params) - - -async def network(ctx: ToolContext, action: str, **params: Any) -> ToolResult: - return await _dispatch_group(ctx, "network", action, **params) - - -async def system(ctx: ToolContext, action: str, **params: Any) -> ToolResult: - return await _dispatch_group(ctx, "system", action, **params) - - -def _make_action_function(action: str): - async def _tool(ctx: ToolContext, **kwargs: Any) -> ToolResult: - return await unified_ops(ctx, action=action, **kwargs) - _tool.__name__ = action - return _tool - - -for _action_name in _ACTION_MAP: - globals()[_action_name] = _make_action_function(_action_name) - -del _action_name diff --git a/.flocks/flockshub/plugins/tools/api/sangfor_af_v8_0_48/sangfor_af_v48_auth.yaml b/.flocks/flockshub/plugins/tools/api/sangfor_af_v8_0_48/sangfor_af_v48_auth.yaml deleted file mode 100644 index ecb99db68..000000000 --- a/.flocks/flockshub/plugins/tools/api/sangfor_af_v8_0_48/sangfor_af_v48_auth.yaml +++ /dev/null @@ -1,44 +0,0 @@ -name: sangfor_af_v48_auth -description: > - Sangfor AF v8.0.48 authentication tool. Use the `action` parameter to - login, logout, or keep the session alive. Token is cached automatically - after a successful login. -description_cn: > - 深信服 AF v8.0.48 认证工具。通过 `action` 参数调用登录、注销或 token 保活接口。 - 登录成功后 token 会自动缓存,后续调用无需手动传 token。 -category: custom -enabled: true -requires_confirmation: false -provider: sangfor_af -inputSchema: - type: object - properties: - action: - type: string - description: | - 认证动作名,可选值: - - login - 用途: 登录设备,获取 session token(token 自动缓存) - 必填: 无(用户名/密码从服务配置读取) - 风险提示: 只读认证接口 - 是否任务型: 否 - - logout - 用途: 注销当前登录 session,清除 token 缓存 - 必填: 无 - 风险提示: 写操作,注销后需重新登录 - 是否任务型: 否 - - keepalive - 用途: 刷新 token 超时计时器,保持 session 活跃 - 必填: 无 - 风险提示: 只读接口 - 是否任务型: 否 - enum: - - login - - logout - - keepalive - required: - - action -handler: - type: script - script_file: sangfor_af.handler.py - function: auth diff --git a/.flocks/flockshub/plugins/tools/api/sangfor_af_v8_0_48/sangfor_af_v48_network.yaml b/.flocks/flockshub/plugins/tools/api/sangfor_af_v8_0_48/sangfor_af_v48_network.yaml deleted file mode 100644 index 5dbdd334b..000000000 --- a/.flocks/flockshub/plugins/tools/api/sangfor_af_v8_0_48/sangfor_af_v48_network.yaml +++ /dev/null @@ -1,65 +0,0 @@ -name: sangfor_af_v48_network -description: > - Sangfor AF v8.0.48 network tool. Query routing tables (IPv4 and IPv6) - and network-related status information. -description_cn: > - 深信服 AF v8.0.48 网络工具。通过 `action` 参数查询路由表(IPv4/IPv6) - 及网络相关状态信息。 -category: custom -enabled: true -requires_confirmation: false -provider: sangfor_af -inputSchema: - type: object - properties: - action: - type: string - description: | - 网络查询动作名,可选值: - - get_routes - 用途: 获取后台 IPv4 路由信息列表 - 必填: 无 - 常用: routeType(ALL_ROUTE/STATIC_ROUTE/DIRECT_ROUTE 等)、_start、_length - 风险提示: 只读查询接口 - 是否任务型: 否 - - get_routes_ipv6 - 用途: 获取后台 IPv6 路由信息列表 - 必填: 无 - 常用: routeType、_start、_length - 风险提示: 只读查询接口 - 是否任务型: 否 - enum: - - get_routes - - get_routes_ipv6 - routeType: - type: string - description: > - 路由类型过滤:ALL_ROUTE=所有路由,STATIC_ROUTE=静态路由, - DIRECT_ROUTE=直连路由,OSPF_ROUTE=OSPF路由,RIP_ROUTE=RIP路由, - VPN_ROUTE=VPN路由,SSL_VPN_ROUTE=SSL VPN路由, - IBGP_ROUTE=IBGP路由,EBGP_ROUTE=EBGP路由 - enum: - - ALL_ROUTE - - STATIC_ROUTE - - DIRECT_ROUTE - - OSPF_ROUTE - - RIP_ROUTE - - VPN_ROUTE - - SSL_VPN_ROUTE - - IBGP_ROUTE - - EBGP_ROUTE - _start: - type: integer - description: 分页起始位置(从0开始) - _length: - type: integer - description: 每页最大返回数量(最大200,默认100) - _search: - type: string - description: 模糊搜索关键字 - required: - - action -handler: - type: script - script_file: sangfor_af.handler.py - function: network diff --git a/.flocks/flockshub/plugins/tools/api/sangfor_af_v8_0_48/sangfor_af_v48_objects.yaml b/.flocks/flockshub/plugins/tools/api/sangfor_af_v8_0_48/sangfor_af_v48_objects.yaml deleted file mode 100644 index 69ec70cf5..000000000 --- a/.flocks/flockshub/plugins/tools/api/sangfor_af_v8_0_48/sangfor_af_v48_objects.yaml +++ /dev/null @@ -1,152 +0,0 @@ -name: sangfor_af_v48_objects -description: > - Sangfor AF v8.0.48 objects management tool. Query, create, update, and - delete network IP group objects and services (protocol/port definitions) - used in firewall policies. -description_cn: > - 深信服 AF v8.0.48 对象管理工具。通过 `action` 参数查询、创建、修改和删除 - IP 地址组对象及服务对象(协议/端口定义),这些对象被防火墙策略引用。 -category: custom -enabled: true -requires_confirmation: true -provider: sangfor_af -inputSchema: - type: object - properties: - action: - type: string - description: | - 对象管理动作名,可选值: - - ## IP 地址组 - - get_ipgroups - 用途: 查询符合条件的 IP 地址组列表 - 必填: 无 - 常用: _start、_length、businessType、__nameprefix、important、_search - 风险提示: 只读查询接口 - 是否任务型: 否 - - get_ipgroup - 用途: 获取单个 IP 地址组详情 - 必填: uuid - 风险提示: 只读查询接口 - 是否任务型: 否 - - create_ipgroup - 用途: 创建新的 IP 地址组 - 必填: name、businessType - 常用: ipRanges、addressType、description、important - 风险提示: 写操作;创建后可被防火墙策略引用 - 是否任务型: 否 - - update_ipgroup - 用途: 增量更新(PATCH)指定 IP 地址组 - 必填: uuid - 常用: name、ipRanges、description - 风险提示: 写操作;修改 IP 组会影响引用该组的所有策略 - 是否任务型: 否 - - delete_ipgroup - 用途: 删除指定 IP 地址组 - 必填: uuid - 风险提示: 高风险写操作;如有策略引用该组将删除失败 - 是否任务型: 否 - - ## 服务对象 - - get_services - 用途: 查询服务或服务组列表(预定义或自定义) - 必填: 无 - 常用: _start、_length、_search、serviceType - 风险提示: 只读查询接口 - 是否任务型: 否 - - get_service - 用途: 获取单个服务或服务组详情 - 必填: uuid - 风险提示: 只读查询接口 - 是否任务型: 否 - enum: - - get_ipgroups - - get_ipgroup - - create_ipgroup - - update_ipgroup - - delete_ipgroup - - get_services - - get_service - - uuid: - type: string - description: IP地址组或服务对象的唯一标识符(32字符UUID) - name: - type: string - description: 对象名称(最大95字符) - businessType: - type: string - description: > - IP地址组业务类型:IP=IP地址,ADDRGROUP=地址组, - USER=用户地址,BUSINESS=业务地址 - enum: - - IP - - ADDRGROUP - - USER - - BUSINESS - addressType: - type: string - description: "IP协议版本:IPV4 或 IPV6" - enum: - - IPV4 - - IPV6 - important: - type: string - description: "重要级别:COMMON=普通,CORE=核心" - enum: - - COMMON - - CORE - ipRanges: - type: array - items: - type: object - properties: - start: - type: string - description: IP范围起始地址(如 192.168.1.1) - end: - type: string - description: IP范围结束地址(如 192.168.1.254) - description: IP地址范围列表 - description: - type: string - description: 对象描述(最大95字符) - creator: - type: string - description: 创建者名称 - serviceType: - type: string - description: "服务类型过滤:SERVICE=单个服务,SERVICEGROUP=服务组" - enum: - - SERVICE - - SERVICEGROUP - - # Pagination - _start: - type: integer - description: 分页起始位置(从0开始) - _length: - type: integer - description: 每页最大返回数量(最大200,默认100) - __nameprefix: - type: string - description: 按名称前缀过滤(最大95字符) - _search: - type: string - description: 模糊搜索关键字(最大95字符) - _order: - type: string - description: "排序方向:asc 或 desc" - enum: - - asc - - desc - _sortby: - type: string - description: 排序字段名 - required: - - action -handler: - type: script - script_file: sangfor_af.handler.py - function: objects diff --git a/.flocks/flockshub/plugins/tools/api/sangfor_af_v8_0_48/sangfor_af_v48_ops.yaml b/.flocks/flockshub/plugins/tools/api/sangfor_af_v8_0_48/sangfor_af_v48_ops.yaml deleted file mode 100644 index 6ae193b4f..000000000 --- a/.flocks/flockshub/plugins/tools/api/sangfor_af_v8_0_48/sangfor_af_v48_ops.yaml +++ /dev/null @@ -1,165 +0,0 @@ -name: sangfor_af_v48_ops -description: > - Sangfor AF v8.0.48 operations center tool. Manages blacklist/whitelist - entries (IPs, domains, URLs) and blocked attacker IPs via the `action` - parameter. Key security triage actions for SOC workflows. -description_cn: > - 深信服 AF v8.0.48 运营中心工具。通过 `action` 参数管理黑白名单(IP/域名/URL) - 和封锁攻击者 IP。是 SOC 安全处置的核心接口。 -category: custom -enabled: true -requires_confirmation: true -provider: sangfor_af -inputSchema: - type: object - properties: - action: - type: string - description: | - 运营中心动作名,可选值: - - ## 黑白名单管理 - - get_blackwhitelist - 用途: 查询黑白名单列表(IP/域名/URL) - 必填: 无 - 常用: type(BLACK/WHITE)、_start、_length - 风险提示: 只读查询接口 - 是否任务型: 否 - - add_blackwhitelist - 用途: 添加单条黑白名单 - 必填: url(IP/域名/URL)、type(BLACK/WHITE) - 常用: enable、description、domain(0=IP,1=域名,2=URL) - 风险提示: 写操作;添加黑名单会拦截对应流量 - 是否任务型: 否 - - batch_add_blackwhitelist - 用途: 批量添加黑白名单 - 必填: items(数组,每项含 url/type 字段) - 风险提示: 写操作,批量添加黑名单影响面大 - 是否任务型: 否 - - delete_blackwhitelist - 用途: 删除单条黑白名单 - 必填: url(条目的 IP/域名/URL) - 常用: type(BLACK/WHITE) - 风险提示: 写操作,删除白名单可能导致误拦截 - 是否任务型: 否 - - batch_delete_blackwhitelist - 用途: 批量删除黑白名单 - 必填: items(数组,每项含 url 字段) - 风险提示: 写操作,批量删除影响面大 - 是否任务型: 否 - - ## 封锁攻击者 IP - - get_blockip_list - 用途: 查询当前封锁攻击者 IP 列表 - 必填: 无 - 常用: _start、_length、fuzzyIP(模糊搜索)、creator(AF/SIP) - 风险提示: 只读查询接口 - 是否任务型: 否 - - batch_add_blockip - 用途: 批量封锁攻击者 IP - 必填: items(数组,每项含 srcIP、dstIP 等字段) - 常用: aifwType(MANUAL/AUTO) - 风险提示: 高风险写操作;封锁 IP 会拦截其所有流量 - 是否任务型: 否 - - batch_delete_blockip - 用途: 批量解封攻击者 IP - 必填: items(数组,每项含 srcIP、dstIP 等字段) - 风险提示: 写操作,解封恶意 IP 存在安全风险 - 是否任务型: 否 - - clear_blockip - 用途: 清空封锁攻击者 IP 列表 - 必填: 无 - 常用: creator(AF/SIP,指定清除哪类封锁) - 风险提示: 高风险写操作;会清除所有封锁 IP - 是否任务型: 否 - - get_blockip_auto_config - 用途: 获取自动封锁攻击者时长配置 - 必填: 无 - 风险提示: 只读查询接口 - 是否任务型: 否 - - set_blockip_auto_config - 用途: 修改自动封锁攻击者时长 - 必填: blockTime(封锁时长,单位秒) - 风险提示: 写操作,影响自动封锁策略 - 是否任务型: 否 - enum: - - get_blackwhitelist - - add_blackwhitelist - - batch_add_blackwhitelist - - delete_blackwhitelist - - batch_delete_blackwhitelist - - get_blockip_list - - batch_add_blockip - - batch_delete_blockip - - clear_blockip - - get_blockip_auto_config - - set_blockip_auto_config - - # Blacklist/whitelist params - url: - type: string - description: IP地址、域名或URL(黑白名单条目值) - type: - type: string - description: "名单类型:BLACK(黑名单)或 WHITE(白名单)" - enum: - - BLACK - - WHITE - enable: - type: boolean - description: 是否启用该条目,默认 true - description: - type: string - description: 条目描述信息(最大95字符) - domain: - type: integer - description: "条目类型:0=IP地址,1=域名,2=URL" - enum: [0, 1, 2] - items: - type: array - items: - type: object - description: 批量操作时的条目数组,每项至少包含 url(黑白名单)或 srcIP/dstIP(封锁IP) - - # Block IP params - fuzzyIP: - type: string - description: 模糊搜索IP关键字(最大15字符) - creator: - type: string - description: "封锁来源身份:AF(防火墙自身)或 SIP(安全感知平台)" - enum: - - AF - - SIP - aifwType: - type: string - description: "添加封锁IP的类型:MANUAL(手动)或 AUTO(自动,需要 creator=SIP)" - enum: - - MANUAL - - AUTO - blockTime: - type: integer - description: 自动封锁时长(秒) - - # Pagination - _start: - type: integer - description: 分页起始位置(从0开始) - _length: - type: integer - description: 每页最大返回数量(最大200,默认100) - _sortby: - type: string - description: 排序字段名 - _order: - type: string - description: "排序方向:asc(升序)或 desc(降序)" - enum: - - asc - - desc - required: - - action -handler: - type: script - script_file: sangfor_af.handler.py - function: ops diff --git a/.flocks/flockshub/plugins/tools/api/sangfor_af_v8_0_48/sangfor_af_v48_status.yaml b/.flocks/flockshub/plugins/tools/api/sangfor_af_v8_0_48/sangfor_af_v48_status.yaml deleted file mode 100644 index f09276a15..000000000 --- a/.flocks/flockshub/plugins/tools/api/sangfor_af_v8_0_48/sangfor_af_v48_status.yaml +++ /dev/null @@ -1,91 +0,0 @@ -name: sangfor_af_v48_status -description: > - Sangfor AF v8.0.48 device status tool. Query system resource usage - (CPU, memory, disk), firmware version, network interface status, - current time, and system uptime. -description_cn: > - 深信服 AF v8.0.48 状态中心工具。通过 `action` 参数查询系统资源(CPU/内存/磁盘)、 - 固件版本、网口状态、当前时间及系统运行时长等信息。 -category: custom -enabled: true -requires_confirmation: false -provider: sangfor_af -inputSchema: - type: object - properties: - action: - type: string - description: | - 状态查询动作名,可选值: - - get_memory_usage - 用途: 获取当前内存使用率(百分比) - 必填: 无 - 风险提示: 只读查询接口 - 是否任务型: 否 - - get_cpu_usage - 用途: 获取当前 CPU 使用率(百分比) - 必填: 无 - 风险提示: 只读查询接口 - 是否任务型: 否 - - get_disk_usage - 用途: 获取当前磁盘使用率(百分比) - 必填: 无 - 风险提示: 只读查询接口 - 是否任务型: 否 - - get_system_version - 用途: 获取 AF 系统固件版本信息 - 必填: 无 - 常用: filter(ALL/FULL/MAJOR/MINOR 等) - 风险提示: 只读查询接口 - 是否任务型: 否 - - get_interface_status - 用途: 获取指定网口或全部网口的状态(流速、连接状态) - 必填: 无 - 常用: interfaceNames(如 eth0,不传则获取全部) - 风险提示: 只读查询接口 - 是否任务型: 否 - - get_runtime_status - 用途: 获取系统运行时长(uptime) - 必填: 无 - 风险提示: 只读查询接口 - 是否任务型: 否 - - get_current_time - 用途: 获取设备当前时间 - 必填: 无 - 风险提示: 只读查询接口 - 是否任务型: 否 - enum: - - get_memory_usage - - get_cpu_usage - - get_disk_usage - - get_system_version - - get_interface_status - - get_runtime_status - - get_current_time - filter: - type: string - description: > - 版本信息过滤(仅用于 get_system_version): - ALL=显示所有,FULL=完整版本号,MAJOR=主版本号,MINOR=次版本号, - INCREASE=增版本号,BUILD=创建日期,EN=是否英文版,HF=是否HF版,B=是否Beta版 - enum: - - ALL - - FULL - - MAJOR - - MINOR - - INCREASE - - BUILD - - EN - - HF - - B - - R - - ADD - interfaceNames: - type: string - description: 网口名称(如 eth0),用于 get_interface_status;不填则获取全部接口 - required: - - action -handler: - type: script - script_file: sangfor_af.handler.py - function: status diff --git a/.flocks/flockshub/plugins/tools/api/sangfor_af_v8_0_48/sangfor_af_v48_system.yaml b/.flocks/flockshub/plugins/tools/api/sangfor_af_v8_0_48/sangfor_af_v48_system.yaml deleted file mode 100644 index e41c6c291..000000000 --- a/.flocks/flockshub/plugins/tools/api/sangfor_af_v8_0_48/sangfor_af_v48_system.yaml +++ /dev/null @@ -1,53 +0,0 @@ -name: sangfor_af_v48_system -description: > - Sangfor AF v8.0.48 system management tool. Query and manage administrator - accounts on the AF device. -description_cn: > - 深信服 AF v8.0.48 系统管理工具。通过 `action` 参数查询和管理 AF 设备上的 - 管理员账户信息。 -category: custom -enabled: true -requires_confirmation: true -provider: sangfor_af -inputSchema: - type: object - properties: - action: - type: string - description: | - 系统管理动作名,可选值: - - get_accounts - 用途: 查询所有管理员账户列表 - 必填: 无 - 常用: _start、_length、enable - 风险提示: 只读查询接口 - 是否任务型: 否 - - get_account - 用途: 查询指定管理员账户详情 - 必填: name(账户名) - 风险提示: 只读查询接口 - 是否任务型: 否 - enum: - - get_accounts - - get_account - name: - type: string - description: 管理员账户名(用于 get_account) - enable: - type: boolean - description: 按启用/禁用状态过滤账户 - _start: - type: integer - description: 分页起始位置(从0开始) - _length: - type: integer - description: 每页最大返回数量 - _search: - type: string - description: 模糊搜索关键字 - required: - - action -handler: - type: script - script_file: sangfor_af.handler.py - function: system diff --git a/.flocks/flockshub/plugins/tools/api/sangfor_af_v8_0_85/_provider.yaml b/.flocks/flockshub/plugins/tools/api/sangfor_af_v8_0_85/_provider.yaml deleted file mode 100644 index b3566602a..000000000 --- a/.flocks/flockshub/plugins/tools/api/sangfor_af_v8_0_85/_provider.yaml +++ /dev/null @@ -1,52 +0,0 @@ -name: sangfor_af -service_id: sangfor_af -version: "8.0.85" -description: > - Sangfor AF (Application Firewall) v8.0.85 REST API service. - Uses session-based cookie authentication: call login to obtain - a token, then supply it via Cookie header in subsequent requests. - This version adds monitoring (session/traffic/statistics) APIs - compared to v8.0.48. -description_cn: > - 深信服 AF 下一代防火墙 v8.0.85 REST API 服务(对应 AF8.0.95 发布版文档)。 - 使用基于 Session 的 Cookie 认证:首先调用 login 获取 token, - 后续请求在 Cookie 中携带 token。在 v8.0.48 基础上新增监控(会话/流量/统计)相关 API。 -auth: - type: custom - secret: sangfor_af_v8_0_85_username - secret_secret: sangfor_af_v8_0_85_password -credential_fields: - - key: username - label: 管理员用户名 - storage: secret - config_key: username - secret_id: sangfor_af_v8_0_85_username - input_type: text - required: true - - key: password - label: 管理员密码 - storage: secret - config_key: password - secret_id: sangfor_af_v8_0_85_password - input_type: password - required: true - - key: base_url - label: 设备地址 (Base URL) - storage: config - config_key: base_url - input_type: url - default: "https://192.168.1.1" -defaults: - base_url: "https://192.168.1.1" - timeout: 60 - category: custom - product_version: "8.0.85" -notes: | - 深信服 AF v8.0.85 API 认证流程(同 v8.0.48): - 1. 在 AF WebUI「系统 → 管理员账号」勾选 WEBAPI 权限。 - 2. Handler 自动用用户名/密码换取 token 并缓存(带 keepalive 自动续期)。 - 3. 后续所有请求由 Handler 自动注入 Cookie: token=。 - 4. token 默认 10 分钟无操作后失效,缓存失效会自动重新登录。 - 5. 本版本在 v8.0.48 基础上新增了监控相关 API(会话/流量统计等)。 - - `verify_ssl` 由表单底部「SSL 验证」开关控制(默认关闭,与 sangfor_sip 一致)。 diff --git a/.flocks/flockshub/plugins/tools/api/sangfor_af_v8_0_85/_test.yaml b/.flocks/flockshub/plugins/tools/api/sangfor_af_v8_0_85/_test.yaml deleted file mode 100644 index 2e70f0c4a..000000000 --- a/.flocks/flockshub/plugins/tools/api/sangfor_af_v8_0_85/_test.yaml +++ /dev/null @@ -1,100 +0,0 @@ -schema_version: 1 -provider: sangfor_af - -connectivity: - tool: sangfor_af_v85_status - params: - action: get_system_version - -fixtures: - sangfor_af_v85_auth: - - label: "Session keepalive" - label_cn: "刷新 Session 保活" - tags: [smoke] - params: - action: keepalive - assert: - success: true - - sangfor_af_v85_status: - - label: "Get system version" - label_cn: "获取系统版本信息" - tags: [smoke] - params: - action: get_system_version - assert: - success: true - - - label: "Get CPU usage" - label_cn: "获取 CPU 使用率" - tags: [smoke] - params: - action: get_cpu_usage - - sangfor_af_v85_monitor: - - label: "Get session summary" - label_cn: "获取会话概要信息" - tags: [smoke, monitor] - params: - action: get_session_summary - assert: - success: true - - - label: "Get daily new sessions" - label_cn: "获取每日新建会话信息" - tags: [monitor] - params: - action: get_session_dailys - - - label: "Get user traffic top 10" - label_cn: "获取用户流量排行前10名" - tags: [monitor, traffic] - params: - action: get_user_traffic_rank - topNumber: 10 - - - label: "Get app traffic ranking" - label_cn: "获取应用流量排行" - tags: [monitor, traffic] - params: - action: get_app_traffic_rank - - - label: "Get active sessions" - label_cn: "获取实时活跃会话列表" - tags: [monitor, session] - params: - action: get_sessions - - sangfor_af_v85_ops: - - label: "List blocked attacker IPs" - label_cn: "查询封锁攻击者 IP 列表" - tags: [smoke, blockip] - params: - action: get_blockip_list - assert: - success: true - - - label: "List blacklist entries" - label_cn: "查询黑名单列表" - tags: [smoke, blacklist] - params: - action: get_blackwhitelist - type: BLACK - - sangfor_af_v85_objects: - - label: "List IP groups" - label_cn: "查询 IP 地址组列表" - tags: [smoke] - params: - action: get_ipgroups - assert: - success: true - - sangfor_af_v85_network: - - label: "List routing table" - label_cn: "查询路由表" - tags: [smoke] - params: - action: get_routes - assert: - success: true diff --git a/.flocks/flockshub/plugins/tools/api/sangfor_af_v8_0_85/sangfor_af.handler.py b/.flocks/flockshub/plugins/tools/api/sangfor_af_v8_0_85/sangfor_af.handler.py deleted file mode 100644 index 3a5793bd0..000000000 --- a/.flocks/flockshub/plugins/tools/api/sangfor_af_v8_0_85/sangfor_af.handler.py +++ /dev/null @@ -1,681 +0,0 @@ -""" -Sangfor AF (Application Firewall) v8.0.85 API Handler. - -Extends v8.0.48 with additional monitoring APIs: - - Session monitoring (traffic ranking, session counts, session list) - - Statistics (packet loss, buffer, hash table, etc.) - - Log/alarm settings - -Authentication: same as v8.0.48 (session-based Cookie token). -API base URL: https:// -Namespace: /api/v1/namespaces/public/ -Batch ops: /api/batch/v1/namespaces/public/ -""" -from __future__ import annotations - -import os -from typing import Any, Callable, Optional - -import aiohttp - -from flocks.config.config_writer import ConfigWriter -from flocks.tool.registry import ToolContext, ToolResult - -# ── Constants ──────────────────────────────────────────────────────────────── - -SERVICE_ID = "sangfor_af_v8_0_85" -DEFAULT_BASE_URL = "https://192.168.1.1" -DEFAULT_TIMEOUT = 60 -NAMESPACE = "public" - -API_V1 = f"/api/v1/namespaces/{NAMESPACE}" -API_BATCH = f"/api/batch/v1/namespaces/{NAMESPACE}" - -_TOKEN_CACHE: dict[str, str] = {} - - -# ── Secret / Config helpers ─────────────────────────────────────────────────── - -def _get_secret_manager(): - from flocks.security import get_secret_manager - return get_secret_manager() - - -def _resolve_ref(value: Any) -> Optional[str]: - if value is None: - return None - if not isinstance(value, str): - return str(value) - if value.startswith("{secret:") and value.endswith("}"): - return _get_secret_manager().get(value[len("{secret:"): -1]) - if value.startswith("{env:") and value.endswith("}"): - return os.getenv(value[len("{env:"): -1]) - return value - - -def _service_config() -> dict[str, Any]: - raw = ConfigWriter.get_api_service_raw(SERVICE_ID) - return raw if isinstance(raw, dict) else {} - - -def _resolve_verify_ssl(raw: dict[str, Any]) -> bool: - """Read verify_ssl with the same priority as sangfor_sip / onesec: - verify_ssl > ssl_verify > custom_settings.verify_ssl > False. - AF devices commonly use self-signed certs, so default is False. - """ - value = raw.get("verify_ssl") - if value is None: - value = raw.get("ssl_verify") - if value is None: - custom = raw.get("custom_settings") - if isinstance(custom, dict): - value = custom.get("verify_ssl") - if isinstance(value, bool): - return value - if isinstance(value, str): - return value.strip().lower() in {"1", "true", "yes", "on"} - return False - - -def _resolve_runtime_config() -> tuple[str, int, str, str, bool]: - raw = _service_config() - base_url = (_resolve_ref(raw.get("base_url")) or DEFAULT_BASE_URL).rstrip("/") - timeout = raw.get("timeout", DEFAULT_TIMEOUT) - try: - timeout = int(timeout) - except (TypeError, ValueError): - timeout = DEFAULT_TIMEOUT - - sm = _get_secret_manager() - username = ( - _resolve_ref(raw.get("username")) - or sm.get("sangfor_af_v8_0_85_username") - or os.getenv("AF_USERNAME") - ) - password = ( - _resolve_ref(raw.get("password")) - or sm.get("sangfor_af_v8_0_85_password") - or os.getenv("AF_PASSWORD") - ) - if not username or not password: - raise ValueError( - "AF API credentials not configured. " - "Please set username and password in the sangfor_af_v8_0_85 service configuration." - ) - return base_url, timeout, username, password, _resolve_verify_ssl(raw) - - -# ── Session / Token management ──────────────────────────────────────────────── - -async def _login(session, base_url, username, password, verify_ssl): - url = f"{base_url}{API_V1}/login" - try: - async with session.post( - url, - json={"name": username, "password": password}, - ssl=verify_ssl, - ) as resp: - data = await resp.json(content_type=None) - except aiohttp.ClientError as exc: - return None, f"AF login request failed: {exc}" - code = data.get("code") - if code != 0: - return None, f"AF login failed (code={code}): {data.get('message', 'Unknown error')}" - token = data.get("data", {}).get("loginResult", {}).get("token") - if not token: - return None, "AF login succeeded but no token returned" - return token, None - - -async def _get_token(session, base_url, username, password, verify_ssl): - cached = _TOKEN_CACHE.get(base_url) - if cached: - try: - async with session.get( - f"{base_url}{API_V1}/keepalive", - headers={"Cookie": f"token={cached}"}, - ssl=verify_ssl, - ) as resp: - ka = await resp.json(content_type=None) - if ka.get("code") == 0: - return cached, None - except Exception: - pass - token, err = await _login(session, base_url, username, password, verify_ssl) - if err: - return None, err - _TOKEN_CACHE[base_url] = token - return token, None - - -# ── Low-level HTTP ──────────────────────────────────────────────────────────── - -def _pick(params: dict[str, Any], *keys: str) -> dict[str, Any]: - return {k: params[k] for k in keys if k in params and params[k] is not None} - - -def _af_result(action: str, payload: Any, version: str = "8.0.85") -> ToolResult: - metadata = {"source": "Sangfor AF", "api": action, "version": version} - if isinstance(payload, dict): - code = payload.get("code") - if code not in (None, 0): - msg = payload.get("message", "Unknown error") - return ToolResult(success=False, error=f"AF API error (code={code}): {msg}", metadata=metadata) - return ToolResult(success=True, output=payload.get("data", payload), metadata=metadata) - return ToolResult(success=True, output=payload, metadata=metadata) - - -async def _call( - method: str, - path: str, - params: Optional[dict[str, Any]] = None, - json: Optional[Any] = None, - action: str = "", -) -> ToolResult: - try: - base_url, timeout, username, password, verify_ssl = _resolve_runtime_config() - except ValueError as exc: - return ToolResult(success=False, error=str(exc)) - - headers = {"Content-Type": "application/json"} - async with aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=timeout)) as session: - token, err = await _get_token(session, base_url, username, password, verify_ssl) - if err: - return ToolResult(success=False, error=err) - headers["Cookie"] = f"token={token}" - url = f"{base_url}{path}" - try: - async with session.request( - method.upper(), url, params=params, json=json, headers=headers, ssl=verify_ssl, - ) as resp: - if resp.status >= 400: - text = await resp.text() - return ToolResult(success=False, error=f"HTTP {resp.status}: {text[:500]}") - data = await resp.json(content_type=None) - except aiohttp.ClientError as exc: - return ToolResult(success=False, error=f"Request failed: {exc}") - except Exception as exc: - return ToolResult(success=False, error=f"Unexpected error: {exc}") - return _af_result(action or path.rsplit("/", 1)[-1], data) - - -# ── Auth actions ────────────────────────────────────────────────────────────── - -async def _do_login(ctx: ToolContext, **params: Any) -> ToolResult: - del ctx - try: - base_url, timeout, username, password, verify_ssl = _resolve_runtime_config() - except ValueError as exc: - return ToolResult(success=False, error=str(exc)) - async with aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=timeout)) as session: - token, err = await _login(session, base_url, username, password, verify_ssl) - if err: - return ToolResult(success=False, error=err) - _TOKEN_CACHE[base_url] = token - return ToolResult( - success=True, - output={"token": token, "message": "Login successful"}, - metadata={"source": "Sangfor AF", "api": "login", "version": "8.0.85"}, - ) - - -async def _do_logout(ctx: ToolContext, **params: Any) -> ToolResult: - del ctx - result = await _call("POST", f"{API_V1}/logout", action="logout") - try: - base_url, *_ = _resolve_runtime_config() - _TOKEN_CACHE.pop(base_url, None) - except ValueError: - pass - return result - - -async def _do_keepalive(ctx: ToolContext, **params: Any) -> ToolResult: - del ctx - return await _call("GET", f"{API_V1}/keepalive", action="keepalive") - - -# ── Objects actions ────────────────────────────────────────────────────────── - -async def _do_get_ipgroups(ctx: ToolContext, **params: Any) -> ToolResult: - del ctx - query = _pick(params, "_start", "_length", "businessType", "__nameprefix", "important", "_search", "_order", "_sortby", "addressType") - return await _call("GET", f"{API_V1}/ipgroups", params=query, action="get_ipgroups") - - -async def _do_get_ipgroup(ctx: ToolContext, **params: Any) -> ToolResult: - del ctx - return await _call("GET", f"{API_V1}/ipgroups/{params.get('uuid', '')}", action="get_ipgroup") - - -async def _do_create_ipgroup(ctx: ToolContext, **params: Any) -> ToolResult: - del ctx - body = _pick(params, "name", "businessType", "description", "addressType", "important", "ipRanges", "creator") - return await _call("POST", f"{API_V1}/ipgroups", json={"obj": body}, action="create_ipgroup") - - -async def _do_update_ipgroup(ctx: ToolContext, **params: Any) -> ToolResult: - del ctx - body = _pick(params, "name", "businessType", "description", "addressType", "important", "ipRanges") - return await _call("PATCH", f"{API_V1}/ipgroups/{params.get('uuid', '')}", json={"obj": body}, action="update_ipgroup") - - -async def _do_delete_ipgroup(ctx: ToolContext, **params: Any) -> ToolResult: - del ctx - return await _call("DELETE", f"{API_V1}/ipgroups/{params.get('uuid', '')}", action="delete_ipgroup") - - -async def _do_get_services(ctx: ToolContext, **params: Any) -> ToolResult: - del ctx - query = _pick(params, "_start", "_length", "_search", "_order", "_sortby", "serviceType") - return await _call("GET", f"{API_V1}/services", params=query, action="get_services") - - -async def _do_get_service(ctx: ToolContext, **params: Any) -> ToolResult: - del ctx - return await _call("GET", f"{API_V1}/services/{params.get('uuid', '')}", action="get_service") - - -# ── Monitoring actions (new in v8.0.85) ────────────────────────────────────── - -async def _do_get_user_traffic_rank(ctx: ToolContext, **params: Any) -> ToolResult: - del ctx - body = _pick(params, "topNumber", "vsys", "line", "applicationType", "filterObject") - return await _call( - "POST", - f"{API_V1}/topusertraffics", - params={"_method": "GET"}, - json=body or {}, - action="get_user_traffic_rank", - ) - - -async def _do_get_ip_traffic_trend(ctx: ToolContext, **params: Any) -> ToolResult: - del ctx - # /iptraffics is not a paged endpoint; _start/_length must not be sent. - # topNumber must be int — AF returns code=1001 for any non-int value. - query = _pick(params, "vsys", "topNumber", "unit", "minutes") - if "topNumber" in query: - try: - query["topNumber"] = int(query["topNumber"]) - except (TypeError, ValueError): - pass - return await _call("GET", f"{API_V1}/iptraffics", params=query or None, action="get_ip_traffic_trend") - - -async def _do_get_app_traffic_rank(ctx: ToolContext, **params: Any) -> ToolResult: - del ctx - query = _pick(params, "vsys", "line", "topNumber") - if "topNumber" in query: - try: - query["topNumber"] = int(query["topNumber"]) - except (TypeError, ValueError): - pass - return await _call("GET", f"{API_V1}/apptrafficrank", params=query or None, action="get_app_traffic_rank") - - -async def _do_get_session_dailys(ctx: ToolContext, **params: Any) -> ToolResult: - del ctx - query = _pick(params, "vsys", "ip") - return await _call("GET", f"{API_V1}/sessiondailys", params=query or None, action="get_session_dailys") - - -async def _do_get_session_details(ctx: ToolContext, **params: Any) -> ToolResult: - del ctx - # Endpoint needs explicit filters; without them AF returns 1004 "没有返回值". - query = _pick(params, "vsys", "srcIP", "dstIP", "protocol", "srcPort", "dstPort") - return await _call("GET", f"{API_V1}/sessiondetails", params=query or None, action="get_session_details") - - -async def _do_get_session_count_trend(ctx: ToolContext, **params: Any) -> ToolResult: - del ctx - query = _pick(params, "vsys", "minutes") - return await _call("GET", f"{API_V1}/sessioncounttrend", params=query or None, action="get_session_count_trend") - - -async def _do_get_session_src_ip(ctx: ToolContext, **params: Any) -> ToolResult: - del ctx - # srcIP is required; AF returns 1004 "没有返回值" when omitted. - query = _pick(params, "vsys", "srcIP") - return await _call("GET", f"{API_V1}/sessionsrcip", params=query or None, action="get_session_src_ip") - - -async def _do_get_session_count_rank(ctx: ToolContext, **params: Any) -> ToolResult: - del ctx - query = _pick(params, "_start", "_length", "vsys", "topNumber") - return await _call("GET", f"{API_V1}/sessioncountrank", params=query or None, action="get_session_count_rank") - - -async def _do_get_session_summary(ctx: ToolContext, **params: Any) -> ToolResult: - del ctx - query = _pick(params, "vsys") - return await _call("GET", f"{API_V1}/sessionsummary", params=query or None, action="get_session_summary") - - -async def _do_get_monitor_ips(ctx: ToolContext, **params: Any) -> ToolResult: - del ctx - query = _pick(params, "_start", "_length") - return await _call("GET", f"{API_V1}/monitorips", params=query or None, action="get_monitor_ips") - - -async def _do_get_sessions(ctx: ToolContext, **params: Any) -> ToolResult: - del ctx - body = _pick(params, "_start", "_length", "vsys", "srcIP", "dstIP", "protocol", "srcPort", "dstPort") - # AF8.0.x requires POST + ?_method=GET for /sessions; plain GET returns 1002. - return await _call("POST", f"{API_V1}/sessions", params={"_method": "GET"}, json=body or {}, action="get_sessions") - - -# Statistics (monitoring sub-section) -async def _do_get_packet_drop_stats(ctx: ToolContext, **params: Any) -> ToolResult: - del ctx - query = _pick(params, "_start", "_length") - return await _call("GET", f"{API_V1}/mbufdroppointstatistics", params=query or None, action="get_packet_drop_stats") - - -async def _do_clear_packet_drop_stats(ctx: ToolContext, **params: Any) -> ToolResult: - del ctx - return await _call("DELETE", f"{API_V1}/mbufdroppointstatistics", action="clear_packet_drop_stats") - - -async def _do_get_mbuf_stats(ctx: ToolContext, **params: Any) -> ToolResult: - del ctx - return await _call("GET", f"{API_V1}/mbufstatistics", action="get_mbuf_stats") - - -async def _do_get_hash_table_stats(ctx: ToolContext, **params: Any) -> ToolResult: - del ctx - query = _pick(params, "_start", "_length") - return await _call("GET", f"{API_V1}/hashtablestatistics", params=query or None, action="get_hash_table_stats") - - -# ── Operations center actions ───────────────────────────────────────────────── - -async def _do_get_blackwhitelist(ctx: ToolContext, **params: Any) -> ToolResult: - del ctx - query = _pick(params, "type", "_start", "_length", "_search", "_order", "description") - return await _call("GET", f"{API_V1}/whiteblacklist", params=query, action="get_blackwhitelist") - - -async def _do_add_blackwhitelist(ctx: ToolContext, **params: Any) -> ToolResult: - del ctx - body = _pick(params, "url", "type", "enable", "description", "domain") - return await _call("POST", f"{API_V1}/whiteblacklist", json={"obj": body}, action="add_blackwhitelist") - - -async def _do_batch_add_blackwhitelist(ctx: ToolContext, **params: Any) -> ToolResult: - del ctx - return await _call("POST", f"{API_BATCH}/whiteblacklist", json=params.get("items", []), action="batch_add_blackwhitelist") - - -async def _do_delete_blackwhitelist(ctx: ToolContext, **params: Any) -> ToolResult: - del ctx - url_param = params.get("url", "") - list_type = params.get("type", "") - query = {"type": list_type} if list_type else None - return await _call("DELETE", f"{API_V1}/whiteblacklist/{url_param}", params=query, action="delete_blackwhitelist") - - -async def _do_batch_delete_blackwhitelist(ctx: ToolContext, **params: Any) -> ToolResult: - del ctx - return await _call("POST", f"{API_BATCH}/whiteblacklist", params={"_method": "DELETE"}, json=params.get("items", []), action="batch_delete_blackwhitelist") - - -async def _do_get_blockip_list(ctx: ToolContext, **params: Any) -> ToolResult: - del ctx - query = _pick(params, "_start", "_length", "_sortby", "_order", "creator", "fuzzyIP") - return await _call("GET", f"{API_V1}/blockip", params=query, action="get_blockip_list") - - -async def _do_batch_add_blockip(ctx: ToolContext, **params: Any) -> ToolResult: - del ctx - query = _pick(params, "aifwType") - return await _call("POST", f"{API_BATCH}/blockip", params=query or None, json=params.get("items", []), action="batch_add_blockip") - - -async def _do_batch_delete_blockip(ctx: ToolContext, **params: Any) -> ToolResult: - del ctx - return await _call("POST", f"{API_BATCH}/blockip", params={"_method": "DELETE"}, json=params.get("items", []), action="batch_delete_blockip") - - -async def _do_clear_blockip(ctx: ToolContext, **params: Any) -> ToolResult: - del ctx - query = _pick(params, "creator") - return await _call("DELETE", f"{API_V1}/blockip", params=query or None, action="clear_blockip") - - -async def _do_get_blockip_auto_config(ctx: ToolContext, **params: Any) -> ToolResult: - del ctx - return await _call("GET", f"{API_V1}/blockip/autoconfig", action="get_blockip_auto_config") - - -async def _do_set_blockip_auto_config(ctx: ToolContext, **params: Any) -> ToolResult: - del ctx - return await _call("PUT", f"{API_V1}/blockip/autoconfig", json={"obj": _pick(params, "blockTime")}, action="set_blockip_auto_config") - - -# ── Status actions ──────────────────────────────────────────────────────────── - -async def _do_get_memory_usage(ctx: ToolContext, **params: Any) -> ToolResult: - del ctx - return await _call("GET", f"{API_V1}/memoryusage", action="get_memory_usage") - - -async def _do_get_cpu_usage(ctx: ToolContext, **params: Any) -> ToolResult: - del ctx - return await _call("GET", f"{API_V1}/cpuusage", action="get_cpu_usage") - - -async def _do_get_disk_usage(ctx: ToolContext, **params: Any) -> ToolResult: - del ctx - return await _call("GET", f"{API_V1}/diskusage", action="get_disk_usage") - - -async def _do_get_system_version(ctx: ToolContext, **params: Any) -> ToolResult: - del ctx - query = _pick(params, "filter") - return await _call("GET", f"{API_V1}/systemversion", params=query or None, action="get_system_version") - - -async def _do_get_interface_status(ctx: ToolContext, **params: Any) -> ToolResult: - del ctx - # AF8.0.x: /interfacestatus returns 1002; use /interfaces (list) or - # /interfaces/status?interfaceName= (single interface query). - iface = params.get("interfaceNames") or params.get("interfaceName") or "" - if iface: - return await _call( - "GET", f"{API_V1}/interfaces/status", - params={"interfaceName": iface}, - action="get_interface_status", - ) - return await _call("GET", f"{API_V1}/interfaces", action="get_interface_status") - - -async def _do_get_runtime_status(ctx: ToolContext, **params: Any) -> ToolResult: - del ctx - return await _call("GET", f"{API_V1}/runtimestatus", action="get_runtime_status") - - -async def _do_get_current_time(ctx: ToolContext, **params: Any) -> ToolResult: - del ctx - return await _call("GET", f"{API_V1}/currenttime", action="get_current_time") - - -# ── Network actions ─────────────────────────────────────────────────────────── - -async def _do_get_routes(ctx: ToolContext, **params: Any) -> ToolResult: - del ctx - query = _pick(params, "_start", "_length", "routeType", "_search") - return await _call("GET", f"{API_V1}/routes", params=query or None, action="get_routes") - - -async def _do_get_routes_ipv6(ctx: ToolContext, **params: Any) -> ToolResult: - del ctx - query = _pick(params, "_start", "_length", "routeType", "_search") - return await _call("GET", f"{API_V1}/routes/ipv6", params=query or None, action="get_routes_ipv6") - - -# ── System actions ──────────────────────────────────────────────────────────── - -async def _do_get_accounts(ctx: ToolContext, **params: Any) -> ToolResult: - del ctx - query = _pick(params, "_start", "_length", "_search", "enable") - return await _call("GET", f"{API_V1}/account", params=query or None, action="get_accounts") - - -async def _do_get_account(ctx: ToolContext, **params: Any) -> ToolResult: - del ctx - return await _call("GET", f"{API_V1}/account/{params.get('name', '')}", action="get_account") - - -# ── Action dispatch ─────────────────────────────────────────────────────────── - -_ACTION_MAP: dict[str, Callable] = { - # Auth - "login": _do_login, - "logout": _do_logout, - "keepalive": _do_keepalive, - # Objects - "get_ipgroups": _do_get_ipgroups, - "get_ipgroup": _do_get_ipgroup, - "create_ipgroup": _do_create_ipgroup, - "update_ipgroup": _do_update_ipgroup, - "delete_ipgroup": _do_delete_ipgroup, - "get_services": _do_get_services, - "get_service": _do_get_service, - # Monitoring (new in v8.0.85) - "get_user_traffic_rank": _do_get_user_traffic_rank, - "get_ip_traffic_trend": _do_get_ip_traffic_trend, - "get_app_traffic_rank": _do_get_app_traffic_rank, - "get_session_dailys": _do_get_session_dailys, - "get_session_details": _do_get_session_details, - "get_session_count_trend": _do_get_session_count_trend, - "get_session_src_ip": _do_get_session_src_ip, - "get_session_count_rank": _do_get_session_count_rank, - "get_session_summary": _do_get_session_summary, - "get_monitor_ips": _do_get_monitor_ips, - "get_sessions": _do_get_sessions, - "get_packet_drop_stats": _do_get_packet_drop_stats, - "clear_packet_drop_stats": _do_clear_packet_drop_stats, - "get_mbuf_stats": _do_get_mbuf_stats, - "get_hash_table_stats": _do_get_hash_table_stats, - # Operations center - "get_blackwhitelist": _do_get_blackwhitelist, - "add_blackwhitelist": _do_add_blackwhitelist, - "batch_add_blackwhitelist": _do_batch_add_blackwhitelist, - "delete_blackwhitelist": _do_delete_blackwhitelist, - "batch_delete_blackwhitelist": _do_batch_delete_blackwhitelist, - "get_blockip_list": _do_get_blockip_list, - "batch_add_blockip": _do_batch_add_blockip, - "batch_delete_blockip": _do_batch_delete_blockip, - "clear_blockip": _do_clear_blockip, - "get_blockip_auto_config": _do_get_blockip_auto_config, - "set_blockip_auto_config": _do_set_blockip_auto_config, - # Status - "get_memory_usage": _do_get_memory_usage, - "get_cpu_usage": _do_get_cpu_usage, - "get_disk_usage": _do_get_disk_usage, - "get_system_version": _do_get_system_version, - "get_interface_status": _do_get_interface_status, - "get_runtime_status": _do_get_runtime_status, - "get_current_time": _do_get_current_time, - # Network - "get_routes": _do_get_routes, - "get_routes_ipv6": _do_get_routes_ipv6, - # System - "get_accounts": _do_get_accounts, - "get_account": _do_get_account, -} - -GROUP_ACTIONS: dict[str, set[str]] = { - "auth": {"login", "logout", "keepalive"}, - "objects": {"get_ipgroups", "get_ipgroup", "create_ipgroup", "update_ipgroup", "delete_ipgroup", "get_services", "get_service"}, - "monitor": { - "get_user_traffic_rank", "get_ip_traffic_trend", "get_app_traffic_rank", - "get_session_dailys", "get_session_details", "get_session_count_trend", - "get_session_src_ip", "get_session_count_rank", "get_session_summary", - "get_monitor_ips", "get_sessions", - "get_packet_drop_stats", "clear_packet_drop_stats", - "get_mbuf_stats", "get_hash_table_stats", - }, - "ops": { - "get_blackwhitelist", "add_blackwhitelist", "batch_add_blackwhitelist", - "delete_blackwhitelist", "batch_delete_blackwhitelist", - "get_blockip_list", "batch_add_blockip", "batch_delete_blockip", - "clear_blockip", "get_blockip_auto_config", "set_blockip_auto_config", - }, - "status": { - "get_memory_usage", "get_cpu_usage", "get_disk_usage", - "get_system_version", "get_interface_status", - "get_runtime_status", "get_current_time", - }, - "network": {"get_routes", "get_routes_ipv6"}, - "system": {"get_accounts", "get_account"}, -} - -_CONNECTIVITY_TEST_ACTIONS: dict[str, str] = { - "auth": "keepalive", - "objects": "get_ipgroups", - "monitor": "get_session_summary", - "ops": "get_blackwhitelist", - "status": "get_system_version", - "network": "get_routes", - "system": "get_accounts", -} - - -async def unified_ops(ctx: ToolContext, action: str, **params: Any) -> ToolResult: - handler = _ACTION_MAP.get(action) - if handler is None: - available = ", ".join(sorted(_ACTION_MAP)) - return ToolResult(success=False, error=f"Unknown action: {action}. Available: {available}") - return await handler(ctx, **params) - - -async def _dispatch_group(ctx: ToolContext, group: str, action: str, **params: Any) -> ToolResult: - if action == "test": - return await unified_ops(ctx, action=_CONNECTIVITY_TEST_ACTIONS.get(group, "get_system_version"), **params) - if action not in GROUP_ACTIONS[group]: - available = ", ".join(sorted(GROUP_ACTIONS[group])) - return ToolResult(success=False, error=f"Unsupported {group} action: {action}. Available: {available}") - return await unified_ops(ctx, action=action, **params) - - -async def auth(ctx: ToolContext, action: str, **params: Any) -> ToolResult: - return await _dispatch_group(ctx, "auth", action, **params) - - -async def objects(ctx: ToolContext, action: str, **params: Any) -> ToolResult: - return await _dispatch_group(ctx, "objects", action, **params) - - -async def monitor(ctx: ToolContext, action: str, **params: Any) -> ToolResult: - return await _dispatch_group(ctx, "monitor", action, **params) - - -async def ops(ctx: ToolContext, action: str, **params: Any) -> ToolResult: - return await _dispatch_group(ctx, "ops", action, **params) - - -async def status(ctx: ToolContext, action: str, **params: Any) -> ToolResult: - return await _dispatch_group(ctx, "status", action, **params) - - -async def network(ctx: ToolContext, action: str, **params: Any) -> ToolResult: - return await _dispatch_group(ctx, "network", action, **params) - - -async def system(ctx: ToolContext, action: str, **params: Any) -> ToolResult: - return await _dispatch_group(ctx, "system", action, **params) - - -def _make_action_function(action: str): - async def _tool(ctx: ToolContext, **kwargs: Any) -> ToolResult: - return await unified_ops(ctx, action=action, **kwargs) - _tool.__name__ = action - return _tool - - -for _action_name in _ACTION_MAP: - globals()[_action_name] = _make_action_function(_action_name) - -del _action_name diff --git a/.flocks/flockshub/plugins/tools/api/sangfor_af_v8_0_85/sangfor_af_v85_auth.yaml b/.flocks/flockshub/plugins/tools/api/sangfor_af_v8_0_85/sangfor_af_v85_auth.yaml deleted file mode 100644 index 69718131a..000000000 --- a/.flocks/flockshub/plugins/tools/api/sangfor_af_v8_0_85/sangfor_af_v85_auth.yaml +++ /dev/null @@ -1,44 +0,0 @@ -name: sangfor_af_v85_auth -description: > - Sangfor AF v8.0.48 authentication tool. Use the `action` parameter to - login, logout, or keep the session alive. Token is cached automatically - after a successful login. -description_cn: > - 深信服 AF v8.0.48 认证工具。通过 `action` 参数调用登录、注销或 token 保活接口。 - 登录成功后 token 会自动缓存,后续调用无需手动传 token。 -category: custom -enabled: true -requires_confirmation: false -provider: sangfor_af -inputSchema: - type: object - properties: - action: - type: string - description: | - 认证动作名,可选值: - - login - 用途: 登录设备,获取 session token(token 自动缓存) - 必填: 无(用户名/密码从服务配置读取) - 风险提示: 只读认证接口 - 是否任务型: 否 - - logout - 用途: 注销当前登录 session,清除 token 缓存 - 必填: 无 - 风险提示: 写操作,注销后需重新登录 - 是否任务型: 否 - - keepalive - 用途: 刷新 token 超时计时器,保持 session 活跃 - 必填: 无 - 风险提示: 只读接口 - 是否任务型: 否 - enum: - - login - - logout - - keepalive - required: - - action -handler: - type: script - script_file: sangfor_af.handler.py - function: auth diff --git a/.flocks/flockshub/plugins/tools/api/sangfor_af_v8_0_85/sangfor_af_v85_monitor.yaml b/.flocks/flockshub/plugins/tools/api/sangfor_af_v8_0_85/sangfor_af_v85_monitor.yaml deleted file mode 100644 index 374fac6c6..000000000 --- a/.flocks/flockshub/plugins/tools/api/sangfor_af_v8_0_85/sangfor_af_v85_monitor.yaml +++ /dev/null @@ -1,184 +0,0 @@ -name: sangfor_af_v85_monitor -description: > - Sangfor AF v8.0.85 monitoring tool. Provides real-time and historical - session data, traffic rankings, network statistics, and packet diagnostics. - These APIs are new in v8.0.85 and not available in v8.0.48. -description_cn: > - 深信服 AF v8.0.85 监控工具(v8.0.48 中不含此功能)。通过 `action` 参数 - 查询实时/历史会话数据、流量排行、网络统计及报文诊断信息。 -category: custom -enabled: true -requires_confirmation: false -provider: sangfor_af -inputSchema: - type: object - properties: - action: - type: string - description: | - 监控动作名,可选值: - - ## 流量排行 - - get_user_traffic_rank - 用途: 获取用户流量排行(Top N 用户) - 必填: 无 - 常用: topNumber(前N名,默认10)、vsys、line、applicationType - 风险提示: 只读接口 - 是否任务型: 否 - - get_ip_traffic_trend - 用途: 获取 IP 流量趋势曲线(指定前5或10名IP) - 必填: 无 - 常用: topNumber、vsys、unit、minutes - 风险提示: 只读接口 - 是否任务型: 否 - - get_app_traffic_rank - 用途: 获取应用流量排行(Top N 应用) - 必填: 无 - 常用: topNumber、vsys、line - 风险提示: 只读接口 - 是否任务型: 否 - - ## 会话排行与统计 - - get_session_dailys - 用途: 获取每日新建会话信息 - 必填: 无 - 常用: vsys、ip、_start、_length - 风险提示: 只读接口 - 是否任务型: 否 - - get_session_details - 用途: 获取会话详情列表(含5层信息) - 必填: 无 - 常用: vsys、srcIP、dstIP、protocol、_start、_length - 风险提示: 只读接口 - 是否任务型: 否 - - get_session_count_trend - 用途: 获取会话数量趋势折线图数据 - 必填: 无 - 常用: vsys、minutes(最近N分钟,默认60) - 风险提示: 只读接口 - 是否任务型: 否 - - get_session_src_ip - 用途: 获取指定源IP的会话详情(按目的IP分组) - 必填: 无 - 常用: srcIP、vsys、_start、_length - 风险提示: 只读接口 - 是否任务型: 否 - - get_session_count_rank - 用途: 获取会话数量排行(Top N 源IP) - 必填: 无 - 常用: topNumber、vsys - 风险提示: 只读接口 - 是否任务型: 否 - - get_session_summary - 用途: 获取会话概要信息(总数、协议分布等) - 必填: 无 - 常用: vsys - 风险提示: 只读接口 - 是否任务型: 否 - - get_monitor_ips - 用途: 获取配置中心监听列表IP范围 - 必填: 无 - 常用: _start、_length - 风险提示: 只读接口 - 是否任务型: 否 - - get_sessions - 用途: 获取实时会话列表(当前活跃连接) - 必填: 无 - 常用: vsys、srcIP、dstIP、protocol、srcPort、dstPort、_start、_length - 风险提示: 只读接口 - 是否任务型: 否 - - ## 统计与诊断 - - get_packet_drop_stats - 用途: 获取 mbuf 丢包点统计信息列表 - 必填: 无 - 风险提示: 只读接口 - 是否任务型: 否 - - clear_packet_drop_stats - 用途: 清除后台丢包统计信息 - 必填: 无 - 风险提示: 写操作,清除统计数据不可恢复 - 是否任务型: 否 - - get_mbuf_stats - 用途: 获取 mbuf 内存统计信息 - 必填: 无 - 风险提示: 只读接口 - 是否任务型: 否 - - get_hash_table_stats - 用途: 获取哈希表统计列表 - 必填: 无 - 常用: _start、_length - 风险提示: 只读接口 - 是否任务型: 否 - enum: - - get_user_traffic_rank - - get_ip_traffic_trend - - get_app_traffic_rank - - get_session_dailys - - get_session_details - - get_session_count_trend - - get_session_src_ip - - get_session_count_rank - - get_session_summary - - get_monitor_ips - - get_sessions - - get_packet_drop_stats - - clear_packet_drop_stats - - get_mbuf_stats - - get_hash_table_stats - - topNumber: - type: integer - description: 排行榜取前N名(如5或10) - vsys: - type: string - description: 虚拟系统名称(通常为 public,可省略) - line: - type: integer - description: "线路编号过滤,0=全部(范围0-256)" - applicationType: - type: array - items: - type: string - description: 应用类型过滤列表 - filterObject: - type: object - description: > - 用户流量排行过滤对象: - objectType=GROUP/USER/IP,对应 groups/users/ip 数组 - unit: - type: string - description: "流量单位(如 bps, Kbps, Mbps)" - minutes: - type: integer - description: 查询最近N分钟的数据(默认60) - ip: - type: string - description: IP地址过滤(格式:IPv4/IPv6) - srcIP: - type: string - description: 源IP地址过滤(格式:IPv4/IPv6) - dstIP: - type: string - description: 目的IP地址过滤 - protocol: - type: string - description: "协议过滤:TCP/UDP/ICMP/OTHER" - srcPort: - type: integer - description: 源端口过滤(0-65535) - dstPort: - type: integer - description: 目的端口过滤(0-65535) - _start: - type: integer - description: 分页起始位置(从0开始) - _length: - type: integer - description: 每页最大返回数量(最大200,默认100) - required: - - action -handler: - type: script - script_file: sangfor_af.handler.py - function: monitor diff --git a/.flocks/flockshub/plugins/tools/api/sangfor_af_v8_0_85/sangfor_af_v85_network.yaml b/.flocks/flockshub/plugins/tools/api/sangfor_af_v8_0_85/sangfor_af_v85_network.yaml deleted file mode 100644 index 149f1aee8..000000000 --- a/.flocks/flockshub/plugins/tools/api/sangfor_af_v8_0_85/sangfor_af_v85_network.yaml +++ /dev/null @@ -1,65 +0,0 @@ -name: sangfor_af_v85_network -description: > - Sangfor AF v8.0.48 network tool. Query routing tables (IPv4 and IPv6) - and network-related status information. -description_cn: > - 深信服 AF v8.0.48 网络工具。通过 `action` 参数查询路由表(IPv4/IPv6) - 及网络相关状态信息。 -category: custom -enabled: true -requires_confirmation: false -provider: sangfor_af -inputSchema: - type: object - properties: - action: - type: string - description: | - 网络查询动作名,可选值: - - get_routes - 用途: 获取后台 IPv4 路由信息列表 - 必填: 无 - 常用: routeType(ALL_ROUTE/STATIC_ROUTE/DIRECT_ROUTE 等)、_start、_length - 风险提示: 只读查询接口 - 是否任务型: 否 - - get_routes_ipv6 - 用途: 获取后台 IPv6 路由信息列表 - 必填: 无 - 常用: routeType、_start、_length - 风险提示: 只读查询接口 - 是否任务型: 否 - enum: - - get_routes - - get_routes_ipv6 - routeType: - type: string - description: > - 路由类型过滤:ALL_ROUTE=所有路由,STATIC_ROUTE=静态路由, - DIRECT_ROUTE=直连路由,OSPF_ROUTE=OSPF路由,RIP_ROUTE=RIP路由, - VPN_ROUTE=VPN路由,SSL_VPN_ROUTE=SSL VPN路由, - IBGP_ROUTE=IBGP路由,EBGP_ROUTE=EBGP路由 - enum: - - ALL_ROUTE - - STATIC_ROUTE - - DIRECT_ROUTE - - OSPF_ROUTE - - RIP_ROUTE - - VPN_ROUTE - - SSL_VPN_ROUTE - - IBGP_ROUTE - - EBGP_ROUTE - _start: - type: integer - description: 分页起始位置(从0开始) - _length: - type: integer - description: 每页最大返回数量(最大200,默认100) - _search: - type: string - description: 模糊搜索关键字 - required: - - action -handler: - type: script - script_file: sangfor_af.handler.py - function: network diff --git a/.flocks/flockshub/plugins/tools/api/sangfor_af_v8_0_85/sangfor_af_v85_objects.yaml b/.flocks/flockshub/plugins/tools/api/sangfor_af_v8_0_85/sangfor_af_v85_objects.yaml deleted file mode 100644 index 180b47774..000000000 --- a/.flocks/flockshub/plugins/tools/api/sangfor_af_v8_0_85/sangfor_af_v85_objects.yaml +++ /dev/null @@ -1,152 +0,0 @@ -name: sangfor_af_v85_objects -description: > - Sangfor AF v8.0.48 objects management tool. Query, create, update, and - delete network IP group objects and services (protocol/port definitions) - used in firewall policies. -description_cn: > - 深信服 AF v8.0.48 对象管理工具。通过 `action` 参数查询、创建、修改和删除 - IP 地址组对象及服务对象(协议/端口定义),这些对象被防火墙策略引用。 -category: custom -enabled: true -requires_confirmation: true -provider: sangfor_af -inputSchema: - type: object - properties: - action: - type: string - description: | - 对象管理动作名,可选值: - - ## IP 地址组 - - get_ipgroups - 用途: 查询符合条件的 IP 地址组列表 - 必填: 无 - 常用: _start、_length、businessType、__nameprefix、important、_search - 风险提示: 只读查询接口 - 是否任务型: 否 - - get_ipgroup - 用途: 获取单个 IP 地址组详情 - 必填: uuid - 风险提示: 只读查询接口 - 是否任务型: 否 - - create_ipgroup - 用途: 创建新的 IP 地址组 - 必填: name、businessType - 常用: ipRanges、addressType、description、important - 风险提示: 写操作;创建后可被防火墙策略引用 - 是否任务型: 否 - - update_ipgroup - 用途: 增量更新(PATCH)指定 IP 地址组 - 必填: uuid - 常用: name、ipRanges、description - 风险提示: 写操作;修改 IP 组会影响引用该组的所有策略 - 是否任务型: 否 - - delete_ipgroup - 用途: 删除指定 IP 地址组 - 必填: uuid - 风险提示: 高风险写操作;如有策略引用该组将删除失败 - 是否任务型: 否 - - ## 服务对象 - - get_services - 用途: 查询服务或服务组列表(预定义或自定义) - 必填: 无 - 常用: _start、_length、_search、serviceType - 风险提示: 只读查询接口 - 是否任务型: 否 - - get_service - 用途: 获取单个服务或服务组详情 - 必填: uuid - 风险提示: 只读查询接口 - 是否任务型: 否 - enum: - - get_ipgroups - - get_ipgroup - - create_ipgroup - - update_ipgroup - - delete_ipgroup - - get_services - - get_service - - uuid: - type: string - description: IP地址组或服务对象的唯一标识符(32字符UUID) - name: - type: string - description: 对象名称(最大95字符) - businessType: - type: string - description: > - IP地址组业务类型:IP=IP地址,ADDRGROUP=地址组, - USER=用户地址,BUSINESS=业务地址 - enum: - - IP - - ADDRGROUP - - USER - - BUSINESS - addressType: - type: string - description: "IP协议版本:IPV4 或 IPV6" - enum: - - IPV4 - - IPV6 - important: - type: string - description: "重要级别:COMMON=普通,CORE=核心" - enum: - - COMMON - - CORE - ipRanges: - type: array - items: - type: object - properties: - start: - type: string - description: IP范围起始地址(如 192.168.1.1) - end: - type: string - description: IP范围结束地址(如 192.168.1.254) - description: IP地址范围列表 - description: - type: string - description: 对象描述(最大95字符) - creator: - type: string - description: 创建者名称 - serviceType: - type: string - description: "服务类型过滤:SERVICE=单个服务,SERVICEGROUP=服务组" - enum: - - SERVICE - - SERVICEGROUP - - # Pagination - _start: - type: integer - description: 分页起始位置(从0开始) - _length: - type: integer - description: 每页最大返回数量(最大200,默认100) - __nameprefix: - type: string - description: 按名称前缀过滤(最大95字符) - _search: - type: string - description: 模糊搜索关键字(最大95字符) - _order: - type: string - description: "排序方向:asc 或 desc" - enum: - - asc - - desc - _sortby: - type: string - description: 排序字段名 - required: - - action -handler: - type: script - script_file: sangfor_af.handler.py - function: objects diff --git a/.flocks/flockshub/plugins/tools/api/sangfor_af_v8_0_85/sangfor_af_v85_ops.yaml b/.flocks/flockshub/plugins/tools/api/sangfor_af_v8_0_85/sangfor_af_v85_ops.yaml deleted file mode 100644 index a46585d4a..000000000 --- a/.flocks/flockshub/plugins/tools/api/sangfor_af_v8_0_85/sangfor_af_v85_ops.yaml +++ /dev/null @@ -1,165 +0,0 @@ -name: sangfor_af_v85_ops -description: > - Sangfor AF v8.0.48 operations center tool. Manages blacklist/whitelist - entries (IPs, domains, URLs) and blocked attacker IPs via the `action` - parameter. Key security triage actions for SOC workflows. -description_cn: > - 深信服 AF v8.0.48 运营中心工具。通过 `action` 参数管理黑白名单(IP/域名/URL) - 和封锁攻击者 IP。是 SOC 安全处置的核心接口。 -category: custom -enabled: true -requires_confirmation: true -provider: sangfor_af -inputSchema: - type: object - properties: - action: - type: string - description: | - 运营中心动作名,可选值: - - ## 黑白名单管理 - - get_blackwhitelist - 用途: 查询黑白名单列表(IP/域名/URL) - 必填: 无 - 常用: type(BLACK/WHITE)、_start、_length - 风险提示: 只读查询接口 - 是否任务型: 否 - - add_blackwhitelist - 用途: 添加单条黑白名单 - 必填: url(IP/域名/URL)、type(BLACK/WHITE) - 常用: enable、description、domain(0=IP,1=域名,2=URL) - 风险提示: 写操作;添加黑名单会拦截对应流量 - 是否任务型: 否 - - batch_add_blackwhitelist - 用途: 批量添加黑白名单 - 必填: items(数组,每项含 url/type 字段) - 风险提示: 写操作,批量添加黑名单影响面大 - 是否任务型: 否 - - delete_blackwhitelist - 用途: 删除单条黑白名单 - 必填: url(条目的 IP/域名/URL) - 常用: type(BLACK/WHITE) - 风险提示: 写操作,删除白名单可能导致误拦截 - 是否任务型: 否 - - batch_delete_blackwhitelist - 用途: 批量删除黑白名单 - 必填: items(数组,每项含 url 字段) - 风险提示: 写操作,批量删除影响面大 - 是否任务型: 否 - - ## 封锁攻击者 IP - - get_blockip_list - 用途: 查询当前封锁攻击者 IP 列表 - 必填: 无 - 常用: _start、_length、fuzzyIP(模糊搜索)、creator(AF/SIP) - 风险提示: 只读查询接口 - 是否任务型: 否 - - batch_add_blockip - 用途: 批量封锁攻击者 IP - 必填: items(数组,每项含 srcIP、dstIP 等字段) - 常用: aifwType(MANUAL/AUTO) - 风险提示: 高风险写操作;封锁 IP 会拦截其所有流量 - 是否任务型: 否 - - batch_delete_blockip - 用途: 批量解封攻击者 IP - 必填: items(数组,每项含 srcIP、dstIP 等字段) - 风险提示: 写操作,解封恶意 IP 存在安全风险 - 是否任务型: 否 - - clear_blockip - 用途: 清空封锁攻击者 IP 列表 - 必填: 无 - 常用: creator(AF/SIP,指定清除哪类封锁) - 风险提示: 高风险写操作;会清除所有封锁 IP - 是否任务型: 否 - - get_blockip_auto_config - 用途: 获取自动封锁攻击者时长配置 - 必填: 无 - 风险提示: 只读查询接口 - 是否任务型: 否 - - set_blockip_auto_config - 用途: 修改自动封锁攻击者时长 - 必填: blockTime(封锁时长,单位秒) - 风险提示: 写操作,影响自动封锁策略 - 是否任务型: 否 - enum: - - get_blackwhitelist - - add_blackwhitelist - - batch_add_blackwhitelist - - delete_blackwhitelist - - batch_delete_blackwhitelist - - get_blockip_list - - batch_add_blockip - - batch_delete_blockip - - clear_blockip - - get_blockip_auto_config - - set_blockip_auto_config - - # Blacklist/whitelist params - url: - type: string - description: IP地址、域名或URL(黑白名单条目值) - type: - type: string - description: "名单类型:BLACK(黑名单)或 WHITE(白名单)" - enum: - - BLACK - - WHITE - enable: - type: boolean - description: 是否启用该条目,默认 true - description: - type: string - description: 条目描述信息(最大95字符) - domain: - type: integer - description: "条目类型:0=IP地址,1=域名,2=URL" - enum: [0, 1, 2] - items: - type: array - items: - type: object - description: 批量操作时的条目数组,每项至少包含 url(黑白名单)或 srcIP/dstIP(封锁IP) - - # Block IP params - fuzzyIP: - type: string - description: 模糊搜索IP关键字(最大15字符) - creator: - type: string - description: "封锁来源身份:AF(防火墙自身)或 SIP(安全感知平台)" - enum: - - AF - - SIP - aifwType: - type: string - description: "添加封锁IP的类型:MANUAL(手动)或 AUTO(自动,需要 creator=SIP)" - enum: - - MANUAL - - AUTO - blockTime: - type: integer - description: 自动封锁时长(秒) - - # Pagination - _start: - type: integer - description: 分页起始位置(从0开始) - _length: - type: integer - description: 每页最大返回数量(最大200,默认100) - _sortby: - type: string - description: 排序字段名 - _order: - type: string - description: "排序方向:asc(升序)或 desc(降序)" - enum: - - asc - - desc - required: - - action -handler: - type: script - script_file: sangfor_af.handler.py - function: ops diff --git a/.flocks/flockshub/plugins/tools/api/sangfor_af_v8_0_85/sangfor_af_v85_status.yaml b/.flocks/flockshub/plugins/tools/api/sangfor_af_v8_0_85/sangfor_af_v85_status.yaml deleted file mode 100644 index a1c01a441..000000000 --- a/.flocks/flockshub/plugins/tools/api/sangfor_af_v8_0_85/sangfor_af_v85_status.yaml +++ /dev/null @@ -1,91 +0,0 @@ -name: sangfor_af_v85_status -description: > - Sangfor AF v8.0.48 device status tool. Query system resource usage - (CPU, memory, disk), firmware version, network interface status, - current time, and system uptime. -description_cn: > - 深信服 AF v8.0.48 状态中心工具。通过 `action` 参数查询系统资源(CPU/内存/磁盘)、 - 固件版本、网口状态、当前时间及系统运行时长等信息。 -category: custom -enabled: true -requires_confirmation: false -provider: sangfor_af -inputSchema: - type: object - properties: - action: - type: string - description: | - 状态查询动作名,可选值: - - get_memory_usage - 用途: 获取当前内存使用率(百分比) - 必填: 无 - 风险提示: 只读查询接口 - 是否任务型: 否 - - get_cpu_usage - 用途: 获取当前 CPU 使用率(百分比) - 必填: 无 - 风险提示: 只读查询接口 - 是否任务型: 否 - - get_disk_usage - 用途: 获取当前磁盘使用率(百分比) - 必填: 无 - 风险提示: 只读查询接口 - 是否任务型: 否 - - get_system_version - 用途: 获取 AF 系统固件版本信息 - 必填: 无 - 常用: filter(ALL/FULL/MAJOR/MINOR 等) - 风险提示: 只读查询接口 - 是否任务型: 否 - - get_interface_status - 用途: 获取指定网口或全部网口的状态(流速、连接状态) - 必填: 无 - 常用: interfaceNames(如 eth0,不传则获取全部) - 风险提示: 只读查询接口 - 是否任务型: 否 - - get_runtime_status - 用途: 获取系统运行时长(uptime) - 必填: 无 - 风险提示: 只读查询接口 - 是否任务型: 否 - - get_current_time - 用途: 获取设备当前时间 - 必填: 无 - 风险提示: 只读查询接口 - 是否任务型: 否 - enum: - - get_memory_usage - - get_cpu_usage - - get_disk_usage - - get_system_version - - get_interface_status - - get_runtime_status - - get_current_time - filter: - type: string - description: > - 版本信息过滤(仅用于 get_system_version): - ALL=显示所有,FULL=完整版本号,MAJOR=主版本号,MINOR=次版本号, - INCREASE=增版本号,BUILD=创建日期,EN=是否英文版,HF=是否HF版,B=是否Beta版 - enum: - - ALL - - FULL - - MAJOR - - MINOR - - INCREASE - - BUILD - - EN - - HF - - B - - R - - ADD - interfaceNames: - type: string - description: 网口名称(如 eth0),用于 get_interface_status;不填则获取全部接口 - required: - - action -handler: - type: script - script_file: sangfor_af.handler.py - function: status diff --git a/.flocks/flockshub/plugins/tools/api/sangfor_af_v8_0_85/sangfor_af_v85_system.yaml b/.flocks/flockshub/plugins/tools/api/sangfor_af_v8_0_85/sangfor_af_v85_system.yaml deleted file mode 100644 index 1ad154a95..000000000 --- a/.flocks/flockshub/plugins/tools/api/sangfor_af_v8_0_85/sangfor_af_v85_system.yaml +++ /dev/null @@ -1,53 +0,0 @@ -name: sangfor_af_v85_system -description: > - Sangfor AF v8.0.48 system management tool. Query and manage administrator - accounts on the AF device. -description_cn: > - 深信服 AF v8.0.48 系统管理工具。通过 `action` 参数查询和管理 AF 设备上的 - 管理员账户信息。 -category: custom -enabled: true -requires_confirmation: true -provider: sangfor_af -inputSchema: - type: object - properties: - action: - type: string - description: | - 系统管理动作名,可选值: - - get_accounts - 用途: 查询所有管理员账户列表 - 必填: 无 - 常用: _start、_length、enable - 风险提示: 只读查询接口 - 是否任务型: 否 - - get_account - 用途: 查询指定管理员账户详情 - 必填: name(账户名) - 风险提示: 只读查询接口 - 是否任务型: 否 - enum: - - get_accounts - - get_account - name: - type: string - description: 管理员账户名(用于 get_account) - enable: - type: boolean - description: 按启用/禁用状态过滤账户 - _start: - type: integer - description: 分页起始位置(从0开始) - _length: - type: integer - description: 每页最大返回数量 - _search: - type: string - description: 模糊搜索关键字 - required: - - action -handler: - type: script - script_file: sangfor_af.handler.py - function: system diff --git a/.flocks/flockshub/plugins/tools/device/onesig_v2_5_3_D20250710/_provider.yaml b/.flocks/flockshub/plugins/tools/device/onesig_v2_5_3_D20250710/_provider.yaml index c11832f19..66407e936 100644 --- a/.flocks/flockshub/plugins/tools/device/onesig_v2_5_3_D20250710/_provider.yaml +++ b/.flocks/flockshub/plugins/tools/device/onesig_v2_5_3_D20250710/_provider.yaml @@ -2,6 +2,7 @@ name: onesig_v2_5_3_D20250710 vendor: threatbook service_id: onesig_v2_5_3_D20250710_api version: "2.5.3 D20250710" +integration_type: device description: > OneSIG (Secure Internet Gateway) Web API service — *older v2.5 firmware* variant. Authentication still uses cookie session, but the login endpoint diff --git a/.flocks/flockshub/plugins/tools/device/sangfor_af_v8_0_48/_provider.yaml b/.flocks/flockshub/plugins/tools/device/sangfor_af_v8_0_48/_provider.yaml index 64971d348..2c57b476b 100644 --- a/.flocks/flockshub/plugins/tools/device/sangfor_af_v8_0_48/_provider.yaml +++ b/.flocks/flockshub/plugins/tools/device/sangfor_af_v8_0_48/_provider.yaml @@ -2,6 +2,7 @@ name: sangfor_af vendor: sangfor service_id: sangfor_af version: "8.0.48" +integration_type: device description: > Sangfor AF (Application Firewall) v8.0.48 REST API service. Uses session-based cookie authentication: call login to obtain diff --git a/.flocks/flockshub/plugins/tools/device/sangfor_af_v8_0_85/_provider.yaml b/.flocks/flockshub/plugins/tools/device/sangfor_af_v8_0_85/_provider.yaml index b77adbbc3..111b35034 100644 --- a/.flocks/flockshub/plugins/tools/device/sangfor_af_v8_0_85/_provider.yaml +++ b/.flocks/flockshub/plugins/tools/device/sangfor_af_v8_0_85/_provider.yaml @@ -2,6 +2,7 @@ name: sangfor_af vendor: sangfor service_id: sangfor_af version: "8.0.85" +integration_type: device description: > Sangfor AF (Application Firewall) v8.0.85 REST API service. Uses session-based cookie authentication: call login to obtain diff --git a/flocks/hub/catalog.py b/flocks/hub/catalog.py index 3d0701b96..2f681cc74 100644 --- a/flocks/hub/catalog.py +++ b/flocks/hub/catalog.py @@ -105,6 +105,14 @@ def manifest_path(plugin_type: PluginType, plugin_id: str) -> Path: direct = root / "plugins" / f"{plugin_type}s" / plugin_id / "manifest.json" if direct.is_file(): return direct + # Device plugins ship inside ``plugins/tools/device//`` — there + # is no ``plugins/devices/`` directory. Try the canonical tool path + # before falling back to the index lookup so on-disk manifest files + # (when present) are still picked up. + if plugin_type == "device": + tool_direct = root / "plugins" / "tools" / "device" / plugin_id / "manifest.json" + if tool_direct.is_file(): + return tool_direct manifest_rel = _manifest_path_lookup().get((plugin_type, plugin_id)) if manifest_rel: path = (root / manifest_rel).resolve() @@ -331,6 +339,14 @@ def _tool_tags(plugin_id: str, description: str) -> list[str]: return _safe_tags(inferred) +def _provider_integration_type(provider: dict[str, Any]) -> Optional[str]: + value = provider.get("integration_type") + if not isinstance(value, str): + return None + cleaned = value.strip().lower() + return cleaned or None + + def _tool_manifest(plugin_id: str, root: Path) -> Optional[HubPluginManifest]: if not _has_direct_tool_payload(root): return None @@ -350,8 +366,18 @@ def _tool_manifest(plugin_id: str, root: Path) -> Optional[HubPluginManifest]: for path in sorted(root.iterdir(), key=lambda item: item.name) if path.is_file() and path.suffix in {".yaml", ".yml", ".py"} ] + # ``integration_type: device`` in ``_provider.yaml`` upgrades the + # plugin to the first-class ``device`` Hub type. This drives: + # * the marketplace "Device" tab (filterable as ``type=device``), + # * the standard install path ``/tools/device//`` + # (resolved by ``hub.local.install_root("device", ...)``), + # * the device-access wizard, which still consumes + # ``api_services[storage_key]`` and filters by ``integration_type`` + # so devices keep showing up there too. + integration_type = _provider_integration_type(provider) + plugin_type: PluginType = "device" if integration_type == "device" else "tool" return _base_manifest( - plugin_type="tool", + plugin_type=plugin_type, plugin_id=plugin_id, name=str(provider.get("name") or first_tool.get("name") or plugin_id), description=description, @@ -386,8 +412,12 @@ def _system_plugin_roots() -> dict[tuple[PluginType, str], Path]: tools_root = local.install_root("tool", "project") if tools_root.is_dir(): for directory in sorted((path for path in tools_root.rglob("*") if path.is_dir()), key=lambda item: item.as_posix()): - if _tool_manifest(directory.name, directory): - roots[("tool", directory.name)] = directory + manifest = _tool_manifest(directory.name, directory) + if manifest: + # The manifest type already reflects ``integration_type: + # device`` (see :func:`_tool_manifest`), so we just defer + # to it instead of hardcoding ``"tool"``. + roots[(manifest.type, directory.name)] = directory return roots @@ -426,12 +456,15 @@ def _bundled_tool_roots() -> dict[tuple[PluginType, str], Path]: continue if directory.parent == tools_root and name in {"api", "device", "python", "mcp", "generated"}: continue - if not _tool_manifest(name, directory): + manifest = _tool_manifest(name, directory) + if not manifest: continue # First-wins: keep the highest-priority bundled root entry # and let later collisions fall through silently — same - # contract as ``_system_plugin_roots``. - roots.setdefault(("tool", name), directory) + # contract as ``_system_plugin_roots``. ``manifest.type`` + # honours ``integration_type: device`` so device plugins + # surface with ``("device", id)`` keys. + roots.setdefault((manifest.type, name), directory) return roots @@ -449,7 +482,10 @@ def system_plugin_root(plugin_type: PluginType, plugin_id: str) -> Optional[Path installed = _system_plugin_roots().get((plugin_type, plugin_id)) if installed is not None: return installed - if plugin_type == "tool": + # ``tool`` and ``device`` plugins are both materialised as bundled + # flockshub directories under ``plugins/tools/`` — the device variant + # just sets ``integration_type: device`` in ``_provider.yaml``. + if plugin_type in {"tool", "device"}: return _bundled_tool_roots().get((plugin_type, plugin_id)) return None @@ -468,8 +504,16 @@ def _manifest_for_system_root(plugin_type: PluginType, plugin_id: str, root: Pat return _agent_manifest(plugin_id, root) if plugin_type == "workflow": return _workflow_manifest(plugin_id, root) - if plugin_type == "tool": - return _tool_manifest(plugin_id, root) + if plugin_type in {"tool", "device"}: + # ``_tool_manifest`` infers the final ``device`` vs ``tool`` + # split from ``_provider.yaml``'s ``integration_type``. Callers + # that already know the expected type still pass through here + # so we can confirm the inferred type matches the requested one + # (e.g. avoid returning a device manifest for a tool query). + manifest = _tool_manifest(plugin_id, root) + if manifest and manifest.type != plugin_type: + return None + return manifest return None diff --git a/flocks/hub/installer.py b/flocks/hub/installer.py index ad12dd191..c28cb25f0 100644 --- a/flocks/hub/installer.py +++ b/flocks/hub/installer.py @@ -52,10 +52,21 @@ def _resolve_install_destination( (``api/``, ``python/``, ``mcp/``, ``generated/``) we install to ``///`` — regardless of whether the source is the bundled flockshub copy or an existing project-level install - being re-installed at user scope. All other plugin types and - sources without a recognised group prefix fall back to the - standard ``//`` layout. + being re-installed at user scope. + + For ``plugin_type == "device"`` we always install to + ``/tools/device//`` (resolved through + :func:`local.install_dir`). That keeps every device plugin in a + canonical location regardless of how the source was laid out, and + matches the search root used by + :func:`flocks.config.api_versioning._api_plugin_roots`. + + All other plugin types and sources without a recognised group + prefix fall back to the standard ``//`` layout. """ + if plugin_type == "device": + return local.install_dir(plugin_type, plugin_id, scope) + if plugin_type != "tool": return local.install_dir(plugin_type, plugin_id, scope) @@ -112,7 +123,13 @@ async def _refresh_runtime(plugin_type: PluginType) -> None: from flocks.agent.registry import Agent Agent.invalidate_cache() - elif plugin_type == "tool": + elif plugin_type in {"tool", "device"}: + # ``device`` plugins live under ``/tools/device//`` + # and are loaded by the same ``ToolRegistry`` machinery as ``tool`` + # plugins — refreshing one means refreshing both, so a freshly + # installed device is picked up by both the Tool API summary and + # the Device Access wizard (the latter consumes + # ``api_services[storage_key]`` shaped by ``discover_api_service_descriptors``). from flocks.config.api_versioning import discover_api_service_descriptors from flocks.tool.registry import ToolRegistry @@ -220,8 +237,14 @@ async def uninstall_plugin(plugin_type: PluginType, plugin_id: str) -> bool: raise ValueError("Only user-managed Hub plugin installs can be removed") # Capture provider metadata BEFORE rmtree — once the dir is gone we # can't read its ``_provider.yaml`` to know which api_services keys - # were derived from it. - orphan_keys = _collect_storage_keys(install_path) if plugin_type == "tool" else [] + # were derived from it. ``device`` plugins reuse the same provider + # yaml machinery as ``tool``/``api`` plugins, so we collect orphan + # storage keys for both types. + orphan_keys = ( + _collect_storage_keys(install_path) + if plugin_type in {"tool", "device"} + else [] + ) shutil.rmtree(install_path) local.remove_installed_record(plugin_type, plugin_id) _cleanup_orphan_api_services(orphan_keys) diff --git a/flocks/hub/local.py b/flocks/hub/local.py index 5a58a51ed..68e4ddc3a 100644 --- a/flocks/hub/local.py +++ b/flocks/hub/local.py @@ -28,6 +28,11 @@ def install_root(plugin_type: PluginType, scope: str = "global") -> Path: return root / "agents" if plugin_type == "workflow": return root / "workflows" + if plugin_type == "device": + # Device plugins live as a subdirectory of tools/ so the runtime + # tool loader (which expects ``/tools///``) + # picks them up alongside api/ and python/ groups. + return root / "tools" / "device" return root / "tools" @@ -94,7 +99,7 @@ def has_install_payload(plugin_type: PluginType, path: Path) -> bool: return (path / "agent.yaml").is_file() if plugin_type == "workflow": return (path / "workflow.json").is_file() or (path / "workflow.md").is_file() - if plugin_type == "tool": + if plugin_type in {"tool", "device"}: if path.is_file(): return path.suffix in {".yaml", ".yml", ".py"} return any(candidate.is_file() for candidate in path.rglob("*.yaml")) or any( @@ -139,12 +144,27 @@ def infer_local_install(plugin_type: PluginType, plugin_id: str) -> Optional[Pat for base in (install_root("tool", "global"), install_root("tool", "project")): if not base.is_dir(): continue - for nested in (base / "api" / plugin_id, base / "mcp" / plugin_id, base / "generated" / plugin_id): + for nested in ( + base / "api" / plugin_id, + base / "device" / plugin_id, + base / "mcp" / plugin_id, + base / "generated" / plugin_id, + ): if has_install_payload(plugin_type, nested): return nested for candidate in base.rglob(f"{plugin_id}.yaml"): if has_install_payload(plugin_type, candidate.parent): return candidate.parent + if plugin_type == "device": + # Device installs live under ``/device//``. We already + # checked the canonical path above via ``install_dir``; the loop + # here catches legacy installs that may have been written into + # the bare ``//`` location before ``device`` became + # a first-class plugin type. + for base in (install_root("tool", "global"), install_root("tool", "project")): + legacy = base / plugin_id + if has_install_payload("tool", legacy): + return legacy return None @@ -168,19 +188,33 @@ def infer_local_installs() -> dict[tuple[PluginType, str], Path]: for child in base.iterdir(): if child.is_dir() and has_install_payload("tool", child): result.setdefault(("tool", child.name), child) - for group in ("api", "mcp", "generated"): + # Tools live under ``///`` where ``group`` is + # one of api/device/mcp/generated. ``device`` is a first-class + # plugin type on the Hub layer (driven by ``integration_type: + # device`` in ``_provider.yaml``), so we surface those entries + # keyed as ``("device", id)`` instead of ``("tool", id)`` to keep + # the catalog state in sync with the runtime install path. + for group in ("api", "device", "mcp", "generated"): group_dir = base / group if not group_dir.is_dir(): continue + entry_type: PluginType = "device" if group == "device" else "tool" for child in group_dir.iterdir(): if child.is_dir() and has_install_payload("tool", child): - result.setdefault(("tool", child.name), child) + result.setdefault((entry_type, child.name), child) for candidate in base.rglob("*"): if not candidate.is_file() or candidate.name == "__init__.py": continue if candidate.suffix not in {".yaml", ".yml", ".py"}: continue if has_install_payload("tool", candidate.parent): - result.setdefault(("tool", candidate.stem), candidate.parent) + # Preserve the directory-derived classification populated + # above; never downgrade a previously-classified device + # entry back to tool just because we found another yaml + # in the same package. + stem_key = candidate.stem + if (("device", stem_key) in result) or (("tool", stem_key) in result): + continue + result.setdefault(("tool", stem_key), candidate.parent) return result diff --git a/flocks/hub/models.py b/flocks/hub/models.py index ce2e092be..8227336ea 100644 --- a/flocks/hub/models.py +++ b/flocks/hub/models.py @@ -7,7 +7,7 @@ from pydantic import BaseModel, ConfigDict, Field -PluginType = Literal["skill", "agent", "tool", "workflow"] +PluginType = Literal["skill", "agent", "tool", "device", "workflow"] PluginState = Literal[ "available", "installed", diff --git a/flocks/server/routes/device.py b/flocks/server/routes/device.py index b2adf69ff..924ef5f19 100644 --- a/flocks/server/routes/device.py +++ b/flocks/server/routes/device.py @@ -193,7 +193,14 @@ async def route_update_device(device_id: str, body: DeviceIntegrationUpdate): verify_ssl=new_ssl, db_fields=new_fields, ) - await sync_service_tool_state(row["service_id"]) + # Recompute ``service_id`` from the row's ``storage_key`` instead of + # trusting the stored column. Rows created before the descriptor- + # aware ``storage_key_to_service_id`` fix may carry a too-greedy + # value (e.g. ``onesig`` instead of ``onesig_v2_5_3_D20250710_api``); + # using the column directly would route this sync to the wrong key + # bucket and leave ``api_services[storage_key].enabled`` stale, + # which in turn keeps tools wrongly exposed to the LLM. + await sync_service_tool_state(storage_key_to_service_id(row["storage_key"])) return await route_get_device(device_id) @@ -202,11 +209,13 @@ async def route_delete_device(device_id: str): row = await fetch_device(device_id) if row is None: raise HTTPException(status_code=http_status.HTTP_404_NOT_FOUND, detail="Device not found") - service_id: str = row["service_id"] # Capture storage_key BEFORE deletion: once the row is gone the DB query # inside sync_service_tool_state can no longer see it, so if this was the # last instance for that storage_key the tool would never get disabled. storage_key: str = row["storage_key"] + # Always derive service_id from the live storage_key — see comment in + # ``route_update_device`` for why we don't trust the stored column. + service_id: str = storage_key_to_service_id(storage_key) db_fields: dict = json.loads(row["fields"] or "{}") delete_secrets(device_id, db_fields) diff --git a/flocks/server/routes/provider.py b/flocks/server/routes/provider.py index 1e2206ce3..53e579a27 100644 --- a/flocks/server/routes/provider.py +++ b/flocks/server/routes/provider.py @@ -1205,16 +1205,21 @@ async def update_api_service(provider_id: str, request: APIServiceUpdateRequest) raise HTTPException(status_code=500, detail=str(e)) -def _find_user_installed_tool_plugin_for(storage_key: str) -> Optional[str]: - """Return the ``installed.json`` plugin id that backs *storage_key*. +def _find_user_installed_tool_plugin_for(storage_key: str) -> Optional[tuple[str, str]]: + """Return ``(plugin_type, plugin_id)`` backing *storage_key*, if any. Walks the discovered ``ApiServiceDescriptor`` set, finds the ``_provider.yaml`` matching *storage_key*, and resolves it to the - enclosing user-installed plugin directory id. Returns ``None`` if - no descriptor matches OR the descriptor lives outside - ``~/.flocks/plugins/`` (project-level / built-in installs are kept - intact — only user-level installs cascade). + enclosing user-installed plugin directory id. ``plugin_type`` is + ``"device"`` when the descriptor lives under ``/device/`` or + when ``_provider.yaml`` declares ``integration_type: device`` — + otherwise ``"tool"``. Returns ``None`` if no descriptor matches OR + the descriptor lives outside ``~/.flocks/plugins/`` (project-level + / built-in installs are kept intact — only user-level installs + cascade). """ + import yaml + from flocks.config.api_versioning import discover_api_service_descriptors from flocks.hub import local as hub_local @@ -1228,13 +1233,34 @@ def _find_user_installed_tool_plugin_for(storage_key: str) -> Optional[str]: except ValueError: return None # Not under the user-level install root. plugin_id = plugin_dir.name + + plugin_type = "tool" + if plugin_dir.parent.name == "device": + plugin_type = "device" + else: + try: + provider_yaml = yaml.safe_load(descriptor.provider_yaml.read_text(encoding="utf-8")) + if ( + isinstance(provider_yaml, dict) + and str(provider_yaml.get("integration_type") or "").strip().lower() == "device" + ): + plugin_type = "device" + except Exception: + pass + # Confirm the catalog tracks this as an installed plugin so # we never try to "uninstall" a stray on-disk dir. - record = hub_local.get_record("tool", plugin_id) + record = hub_local.get_record(plugin_type, plugin_id) if record is not None: - return plugin_id - if hub_local.infer_local_install("tool", plugin_id) is not None: - return plugin_id + return (plugin_type, plugin_id) + # Fall back to the opposite type when the record was saved + # before ``device`` became a first-class Hub type. + legacy_type = "tool" if plugin_type == "device" else "device" + legacy_record = hub_local.get_record(legacy_type, plugin_id) + if legacy_record is not None: + return (legacy_type, plugin_id) + if hub_local.infer_local_install(plugin_type, plugin_id) is not None: + return (plugin_type, plugin_id) return None @@ -1273,7 +1299,7 @@ async def delete_api_service(provider_id: str) -> Dict[str, Any]: # ``remove_api_service`` runs, ``discover_api_service_descriptors`` # still works (it scans ``_provider.yaml`` on disk, not config), # but doing this first keeps the order easy to reason about. - backing_plugin_id = _find_user_installed_tool_plugin_for(provider_id) + backing_plugin = _find_user_installed_tool_plugin_for(provider_id) removed_config = ConfigWriter.remove_api_service(provider_id) @@ -1290,19 +1316,23 @@ async def delete_api_service(provider_id: str) -> Dict[str, Any]: matched_count = _set_api_service_tools_enabled(provider_id, False) uninstalled_plugin = False - if backing_plugin_id: + if backing_plugin is not None: + backing_plugin_type, backing_plugin_id = backing_plugin try: # ``uninstall_plugin`` itself calls # ``_cleanup_orphan_api_services`` for the plugin's # storage_keys, but ``provider_id`` is already gone from # config at this point so that pass is a no-op for it. - uninstalled_plugin = await uninstall_plugin("tool", backing_plugin_id) + uninstalled_plugin = await uninstall_plugin(backing_plugin_type, backing_plugin_id) except Exception as exc: # pragma: no cover - defensive log.warning("api_service.delete.plugin_uninstall_failed", { "provider_id": provider_id, + "plugin_type": backing_plugin_type, "plugin_id": backing_plugin_id, "error": str(exc), }) + else: + backing_plugin_id = None if not removed_config and not deleted_secret and not uninstalled_plugin: raise HTTPException(status_code=404, detail="API service not found") diff --git a/flocks/tool/device/startup.py b/flocks/tool/device/startup.py index 9b50e9442..9a2a5e33f 100644 --- a/flocks/tool/device/startup.py +++ b/flocks/tool/device/startup.py @@ -20,9 +20,45 @@ async def device_startup() -> None: await ensure_default_group() await migrate_from_config() + await _heal_stale_service_ids() await _sync_all() +async def _heal_stale_service_ids() -> None: + """Rewrite ``device_integrations.service_id`` rows that disagree with + the descriptor-aware ``storage_key_to_service_id``. + + Rows written by older builds (or before the plugin's + ``_provider.yaml`` was installed) may carry an over-stripped value — + e.g. ``onesig`` instead of ``onesig_v2_5_3_D20250710_api`` for plugins + whose ``service_id`` already contains its own ``_v…`` token. + + Self-healing once at startup keeps the live route handlers and the + sync loop honest without forcing the user to re-create each device. + """ + try: + from flocks.tool.device.store import storage_key_to_service_id + + async with Storage.connect(Storage.get_db_path()) as db: + cur = await db.execute("SELECT id, storage_key, service_id FROM device_integrations") + rows = await cur.fetchall() + updates: list[tuple[str, str]] = [] + for row in rows: + derived = storage_key_to_service_id(row["storage_key"] or "") + if derived and derived != (row["service_id"] or ""): + updates.append((derived, row["id"])) + for new_sid, dev_id in updates: + await db.execute( + "UPDATE device_integrations SET service_id = ? WHERE id = ?", + (new_sid, dev_id), + ) + if updates: + await db.commit() + log.info("tool.device.startup.service_id_healed", {"count": len(updates)}) + except Exception as exc: + log.warn("tool.device.startup.heal_failed", {"error": str(exc)}) + + async def _sync_all() -> None: """Re-sync tool visibility for every service_id we know about. diff --git a/flocks/tool/device/store.py b/flocks/tool/device/store.py index 7a42cc349..98d8fe41d 100644 --- a/flocks/tool/device/store.py +++ b/flocks/tool/device/store.py @@ -51,9 +51,58 @@ def _bump_revision() -> None: # Key derivation # --------------------------------------------------------------------------- +# Matches the trailing ``_v`` segment added by +# :func:`flocks.config.api_versioning.derive_storage_key`. Kept anchored to the +# end of the string so we only strip the *last* version suffix when falling +# back without descriptor data. +_TRAILING_VERSION_SUFFIX = re.compile(r"_v[A-Za-z0-9]+(?:_[A-Za-z0-9]+)*$") + + def storage_key_to_service_id(storage_key: str) -> str: - """Strip the version suffix: ``sangfor_af_v8_0_106`` → ``sangfor_af``.""" - return re.sub(r"_v[\w.]+$", "", storage_key, flags=re.IGNORECASE) + """Recover the bare ``service_id`` from a ``derive_storage_key`` result. + + Examples:: + + sangfor_af_v8_0_106 → sangfor_af + onesig_api_v2_5_3_D20260321 → onesig_api + onesig_v2_5_3_D20250710_api_v2_5_3_D20250710 → onesig_v2_5_3_D20250710_api + + The last example is the tricky one: when the plugin author has baked + a version into ``service_id`` itself (so the ``_provider.yaml`` + declares ``service_id: onesig_v2_5_3_D20250710_api``), the resulting + storage_key contains *two* ``_v…`` segments. A naive + ``re.sub(r"_v[\\w.]+$", "")`` is greedy from the leftmost ``_v`` and + would strip both segments back to ``onesig``, which then fails to + resolve in :func:`api_service_schema._load_provider_yaml_metadata` + (no descriptor has that bare service_id) — leaving the device-add + form blank. + + To stay correct in that case we consult the descriptor registry + first (an exact ``storage_key → service_id`` mapping, populated by + ``discover_api_service_descriptors``), and only fall back to the + regex heuristic when no descriptor matches *and* the input still + carries a single trailing ``_v…`` suffix. + """ + if not storage_key: + return storage_key + # Prefer the descriptor-driven mapping so we honour whatever + # ``service_id`` the plugin's ``_provider.yaml`` declared. This also + # handles the corner case where the plugin's ``service_id`` already + # contains its own ``_v…`` token and the naive regex would + # over-strip back to a prefix that nothing maps to. + try: + from flocks.config.api_versioning import discover_api_service_descriptors + + for descriptor in discover_api_service_descriptors(): + if descriptor.storage_key == storage_key: + return descriptor.service_id + except Exception: # pragma: no cover - defensive (e.g. test isolation) + pass + + # Fallback: anchored, non-greedy match against the trailing ``_v…`` + # segment. Keeps backward compat for storage keys whose plugin is + # missing from the descriptor cache (e.g. dangling config rows). + return _TRAILING_VERSION_SUFFIX.sub("", storage_key) def _now_ms() -> int: @@ -67,12 +116,19 @@ def _now_ms() -> int: def row_to_device(row: aiosqlite.Row) -> DeviceIntegration: raw_fields: Dict[str, str] = json.loads(row["fields"] or "{}") display, has_value = mask_for_display(raw_fields) + # Recompute service_id from storage_key so historically-wrong rows + # (created before ``storage_key_to_service_id`` learned to consult + # the descriptor cache) self-heal on read. The DB column stays as a + # write-side hint; the canonical mapping is always the descriptor's + # ``service_id`` for the row's ``storage_key``. + storage_key = row["storage_key"] + derived_service_id = storage_key_to_service_id(storage_key) if storage_key else row["service_id"] return DeviceIntegration( id=row["id"], group_id=row["group_id"] or DEFAULT_GROUP_ID, name=row["name"], - storage_key=row["storage_key"], - service_id=row["service_id"], + storage_key=storage_key, + service_id=derived_service_id or row["service_id"], enabled=bool(row["enabled"]), verify_ssl=bool(row["verify_ssl"]), fields=display, diff --git a/tests/hub/test_bundled_tools.py b/tests/hub/test_bundled_tools.py index 4e0aa40dd..a14b1a52b 100644 --- a/tests/hub/test_bundled_tools.py +++ b/tests/hub/test_bundled_tools.py @@ -455,13 +455,51 @@ async def test_finds_user_installed_plugin_by_storage_key(self, isolated_hub): ) await install_plugin("tool", "onesig_v2_5_3_D20250710") - plugin_id = _find_user_installed_tool_plugin_for("onesig_api_v2_5_3_D20250710") - assert plugin_id == "onesig_v2_5_3_D20250710" + result = _find_user_installed_tool_plugin_for("onesig_api_v2_5_3_D20250710") + # ``_find_user_installed_tool_plugin_for`` now returns + # ``(plugin_type, plugin_id)`` so the delete cascade can route + # uninstall to the right type (``tool`` or ``device``). + assert result == ("tool", "onesig_v2_5_3_D20250710") async def test_returns_none_when_storage_key_unknown(self, isolated_hub): from flocks.server.routes.provider import _find_user_installed_tool_plugin_for assert _find_user_installed_tool_plugin_for("nonexistent_v9_9_9") is None + async def test_finds_user_installed_device_plugin(self, isolated_hub): + """Devices (``integration_type: device``) should resolve back as + ``("device", id)`` so the api-service delete cascade uninstalls + them through the device-typed Hub flow. + """ + from flocks.server.routes.provider import _find_user_installed_tool_plugin_for + + bundled_plugin = _write_bundled_tool( + isolated_hub["bundled"], + plugin_id="dev_under_device", + service_id="device_under_test_api", + version="1.0.0", + group="device", + ) + # Mark it as a device plugin via the provider metadata. + provider_yaml = bundled_plugin / "_provider.yaml" + provider_yaml.write_text( + yaml.safe_dump( + { + "name": "dev_under_device", + "service_id": "device_under_test_api", + "version": "1.0.0", + "integration_type": "device", + "description": "device fixture", + }, + allow_unicode=True, + ), + encoding="utf-8", + ) + + await install_plugin("device", "dev_under_device") + + result = _find_user_installed_tool_plugin_for("device_under_test_api_v1_0_0") + assert result == ("device", "dev_under_device") + async def test_returns_none_for_project_level_plugin(self, isolated_hub, monkeypatch): """Project-level plugins (under ``/.flocks/plugins/``) are NOT considered "user-installed" — we don't want a delete from diff --git a/tests/hub/test_hub_catalog.py b/tests/hub/test_hub_catalog.py index 5cb691110..ae4911a1d 100644 --- a/tests/hub/test_hub_catalog.py +++ b/tests/hub/test_hub_catalog.py @@ -40,7 +40,10 @@ def isolated_hub_env(tmp_path: Path, monkeypatch: pytest.MonkeyPatch): def test_bundled_hub_catalog_loads(): entries = list_catalog() assert entries - assert {entry.type for entry in entries} >= {"skill", "agent", "tool", "workflow"} + # ``device`` is a first-class Hub type alongside skill/agent/tool/workflow: + # entries with ``integration_type: device`` in ``_provider.yaml`` surface + # under ``type=device`` instead of ``type=tool``. + assert {entry.type for entry in entries} >= {"skill", "agent", "tool", "device", "workflow"} def test_pentest_agents_are_listed_in_agent_catalog(): @@ -67,7 +70,10 @@ def test_project_builtin_plugins_are_listed_as_installed(): assert by_key[("skill", "tdp-use")].native is True assert by_key[("agent", "ndr-analyst")].state == "installed" assert by_key[("workflow", "tdp_alert_triage")].state == "installed" - assert by_key[("tool", "tdp_v3_3_10")].state == "installed" + # ``tdp_v3_3_10`` declares ``integration_type: device`` in + # ``_provider.yaml``, so it surfaces as a ``device`` plugin (not + # ``tool``) in the Hub catalog. + assert by_key[("device", "tdp_v3_3_10")].state == "installed" manifest = load_manifest("skill", "tdp-use") assert manifest.id == "tdp-use" diff --git a/tests/tool/test_device_store_service_id.py b/tests/tool/test_device_store_service_id.py new file mode 100644 index 000000000..527871e9f --- /dev/null +++ b/tests/tool/test_device_store_service_id.py @@ -0,0 +1,156 @@ +"""Tests for ``storage_key_to_service_id`` and ``row_to_device``. + +Regression: when the plugin author bakes the version into the +``service_id`` itself (so ``_provider.yaml`` declares +``service_id: onesig_v2_5_3_D20250710_api``), the resulting +``storage_key`` carries *two* ``_v…`` segments. The naive greedy regex +``re.sub(r"_v[\\w.]+$", "")`` strips both back to ``onesig`` — which +then fails to resolve any ``_provider.yaml`` metadata, so the +"add device" form renders blank instead of showing the credential +fields. + +These tests lock in the descriptor-aware resolution + the read-time +recompute in :func:`row_to_device` that self-heals already-saved rows. +""" + +from __future__ import annotations + +from pathlib import Path +from unittest.mock import MagicMock + +import pytest +import yaml + +from flocks.tool.device.store import row_to_device, storage_key_to_service_id + + +@pytest.fixture(autouse=True) +def reset_descriptor_cache(monkeypatch, tmp_path): + """Point the descriptor scanner at an isolated tools dir for each test.""" + from flocks.config import api_versioning as versioning + + home = tmp_path / "home" + project = tmp_path / "project" + home.mkdir() + project.mkdir() + monkeypatch.setenv("HOME", str(home)) + monkeypatch.chdir(project) + versioning._reset_descriptor_cache() + yield + versioning._reset_descriptor_cache() + + +def _drop_plugin(home: Path, *, plugin_id: str, service_id: str, version: str) -> None: + plugin_dir = home / ".flocks" / "plugins" / "tools" / "device" / plugin_id + plugin_dir.mkdir(parents=True) + (plugin_dir / "_provider.yaml").write_text( + yaml.safe_dump( + { + "name": plugin_id, + "service_id": service_id, + "version": version, + "integration_type": "device", + "credential_fields": [{"key": "base_url", "label": "Base URL"}], + }, + allow_unicode=True, + ), + encoding="utf-8", + ) + + +class TestStorageKeyToServiceId: + def test_strips_trailing_version_suffix(self): + # No descriptor needed — pure regex fallback path. + assert storage_key_to_service_id("sangfor_af_v8_0_106") == "sangfor_af" + assert storage_key_to_service_id("tdp_api_v3_3_10") == "tdp_api" + assert storage_key_to_service_id("ngsoc_api_v4_15_1") == "ngsoc_api" + + def test_returns_unversioned_input_unchanged(self): + assert storage_key_to_service_id("foo") == "foo" + assert storage_key_to_service_id("") == "" + + def test_descriptor_lookup_prefers_provider_service_id(self, tmp_path): + """When the plugin's ``service_id`` already contains its own + ``_v…`` token, descriptor lookup must return the full declared + ``service_id`` instead of greedily peeling both segments. + + Without the descriptor-aware path, the naive regex collapses + ``onesig_v2_5_3_D20250710_api_v2_5_3_D20250710`` to ``onesig`` + and the device-add form ends up with no credential fields. + """ + _drop_plugin( + Path.home(), + plugin_id="onesig_v2_5_3_D20250710", + service_id="onesig_v2_5_3_D20250710_api", + version="2.5.3 D20250710", + ) + from flocks.config.api_versioning import ( + discover_api_service_descriptors, + ) + + discover_api_service_descriptors(refresh=True) + + result = storage_key_to_service_id( + "onesig_v2_5_3_D20250710_api_v2_5_3_D20250710" + ) + assert result == "onesig_v2_5_3_D20250710_api", ( + "descriptor lookup must restore the plugin's declared " + "service_id even when it contains its own _v… token" + ) + + def test_fallback_when_no_descriptor(self, tmp_path): + """Without a descriptor we fall back to the anchored regex. + + The fallback intentionally stays best-effort — its job is to + avoid crashing for stale config rows whose backing plugin has + already been uninstalled. + """ + # No plugin dropped → empty descriptor cache. + assert ( + storage_key_to_service_id("dangling_plugin_v1_0_0") + == "dangling_plugin" + ) + + +class TestRowToDeviceSelfHeals: + def test_recomputes_service_id_from_storage_key(self, tmp_path): + """A row created before the fix may have a wrong ``service_id`` + stored in the DB (e.g. ``onesig`` instead of + ``onesig_v2_5_3_D20250710_api``). ``row_to_device`` should heal + it on read by recomputing from the row's ``storage_key`` — + otherwise the device-edit form keeps showing blank credentials. + """ + _drop_plugin( + Path.home(), + plugin_id="onesig_v2_5_3_D20250710", + service_id="onesig_v2_5_3_D20250710_api", + version="2.5.3 D20250710", + ) + from flocks.config.api_versioning import ( + discover_api_service_descriptors, + ) + + discover_api_service_descriptors(refresh=True) + + row = MagicMock() + row.__getitem__.side_effect = { + "id": "dev-1", + "group_id": "default-room", + "name": "OneSIG-older", + "storage_key": "onesig_v2_5_3_D20250710_api_v2_5_3_D20250710", + # Intentionally wrong — pretend the row was saved before the fix. + "service_id": "onesig", + "enabled": 1, + "verify_ssl": 0, + "fields": "{}", + "status": "unknown", + "message": None, + "latency_ms": None, + "checked_at": None, + "created_at": 0, + "updated_at": 0, + }.__getitem__ + + device = row_to_device(row) + assert device.service_id == "onesig_v2_5_3_D20250710_api" + assert device.storage_key == "onesig_v2_5_3_D20250710_api_v2_5_3_D20250710" diff --git a/webui/src/api/hub.ts b/webui/src/api/hub.ts index 7b26ebd2f..e9a93aa1e 100644 --- a/webui/src/api/hub.ts +++ b/webui/src/api/hub.ts @@ -1,6 +1,6 @@ import client from './client'; -export type HubPluginType = 'skill' | 'agent' | 'tool' | 'workflow'; +export type HubPluginType = 'skill' | 'agent' | 'tool' | 'device' | 'workflow'; export type HubPluginState = | 'available' | 'installed' diff --git a/webui/src/pages/DeviceIntegration/index.tsx b/webui/src/pages/DeviceIntegration/index.tsx index 92e9b72d6..4c8ef30a9 100644 --- a/webui/src/pages/DeviceIntegration/index.tsx +++ b/webui/src/pages/DeviceIntegration/index.tsx @@ -362,6 +362,16 @@ function DeviceConfigPanel({ device, template, vendorKey, onSave, onDelete, onCl const originalMasked = useRef>({}); const serviceId = device?.service_id ?? template?.id ?? ''; + // ``storage_key`` is the versioned, unambiguous identifier + // (e.g. ``onesig_api_v2_5_3_D20260321``) that tool registrations + // surface as ``ToolInfoResponse.source_name``. Use it directly for + // tool filtering so two devices that happen to share a service_id + // prefix (``onesig`` vs ``onesig_pro``) — or a plugin whose + // ``service_id`` already contains a ``_v…`` token — never bleed each + // other's tools into the device-edit panel. ``template?.id`` is also + // the storage_key (set by the wizard), so the create-mode form + // resolves to the right key too. + const storageKey = device?.storage_key ?? template?.id ?? ''; const vendor = vendorKey ? vendorPresentation(vendorKey) : undefined; useEffect(() => { @@ -391,12 +401,13 @@ function DeviceConfigPanel({ device, template, vendorKey, onSave, onDelete, onCl toolAPI.list() .then((res) => { + // Match against the device's storage_key exactly. The tool + // listing endpoint sets ``source_name = tool.provider``, which + // in turn equals the plugin's storage_key (see + // ``flocks/tool/tool_loader.py``). An exact comparison keeps + // multi-version installs of the same product cleanly isolated. const matched = (res.data || []).filter( - (t) => t.source_name && ( - t.source_name === serviceId || - t.source_name.startsWith(serviceId + '_') || - serviceId.startsWith(t.source_name) - ) + (t) => !!storageKey && t.source_name === storageKey, ); setServiceTools(matched); const initEnabled: Record = {}; @@ -404,7 +415,7 @@ function DeviceConfigPanel({ device, template, vendorKey, onSave, onDelete, onCl setToolEnabled(initEnabled); }) .catch(() => {}); - }, [device, serviceId]); + }, [device, serviceId, storageKey]); const handleSave = async () => { if (!name.trim()) { toast.error('请填写设备名称'); return; } diff --git a/webui/src/pages/Hub/index.tsx b/webui/src/pages/Hub/index.tsx index 92a787892..876a16396 100644 --- a/webui/src/pages/Hub/index.tsx +++ b/webui/src/pages/Hub/index.tsx @@ -57,9 +57,25 @@ const TYPE_LABEL: Record = { skill: 'Skill', agent: 'Agent', tool: 'Tool', + device: 'Device', workflow: 'Workflow', }; +const TYPE_LABEL_CN: Record = { + skill: 'Skill', + agent: 'Agent', + tool: 'Tool', + device: '设备', + workflow: 'Workflow', +}; + +function formatPluginTypeLabel(type: HubPluginType, language: string): string { + if (language.toLowerCase().startsWith('zh')) { + return TYPE_LABEL_CN[type] ?? TYPE_LABEL[type]; + } + return TYPE_LABEL[type]; +} + const HUB_TEXT = { zh: { description: '浏览随 Flocks 打包的本地插件广场,并安装到本机插件目录。', @@ -389,9 +405,9 @@ export default function HubPage() { onChange={value => setTypeFilter(value as HubPluginType | '')} options={[ { value: '', label: text.all }, - ...(['skill', 'agent', 'tool', 'workflow'] as HubPluginType[]).map(type => ({ + ...(['skill', 'agent', 'tool', 'device', 'workflow'] as HubPluginType[]).map(type => ({ value: type, - label: TYPE_LABEL[type], + label: formatPluginTypeLabel(type, i18n.language), count: facetCounts.type[type] ?? 0, })), ]} @@ -628,7 +644,7 @@ function HubTable({ items, actionId, tagLabels, language, text, onSelect, onActi {items.map(item => ( - {TYPE_LABEL[item.type]} + {formatPluginTypeLabel(item.type, language)}