diff --git a/.flocks/flockshub/plugins/tools/device/huaweicloud_waf_v39/_provider.yaml b/.flocks/flockshub/plugins/tools/device/huaweicloud_waf_v39/_provider.yaml new file mode 100644 index 00000000..81d6305a --- /dev/null +++ b/.flocks/flockshub/plugins/tools/device/huaweicloud_waf_v39/_provider.yaml @@ -0,0 +1,71 @@ +name: huaweicloud_waf +vendor: huaweicloud +service_id: huaweicloud_waf_api +version: "39" +integration_type: device +description: > + Huawei Cloud Web Application Firewall (WAF) API service. Configure + Access Key (AK), Secret Key (SK), Region, and Project ID in the credentials + form. The API endpoint is constructed as + `https://waf.{region}.myhuaweicloud.com`. +description_cn: > + 华为云 Web 应用防火墙(WAF)API 服务。在配置页填写 Access Key(AK)、 + Secret Key(SK)、Region(地域,如 `cn-north-4`)和 Project ID(项目 ID)。 + 接口基础地址自动构建为 `https://waf.{region}.myhuaweicloud.com`。 + 也支持直接配置 Token(X-Auth-Token)方式认证。 +auth: + type: custom + secret: huaweicloud_waf_ak + secret_secret: huaweicloud_waf_sk +credential_fields: + - key: ak + label: Access Key (AK) + storage: secret + config_key: ak + secret_id: huaweicloud_waf_ak + input_type: password + required: false + - key: sk + label: Secret Key (SK) + storage: secret + config_key: sk + secret_id: huaweicloud_waf_sk + input_type: password + required: false + - key: token + label: X-Auth-Token(与 AK/SK 二选一) + storage: secret + config_key: token + secret_id: huaweicloud_waf_token + input_type: password + required: false + - key: region + label: Region + storage: config + config_key: region + input_type: text + default: "cn-north-4" + - key: project_id + label: Project ID + storage: config + config_key: project_id + input_type: text + required: true + - key: enterprise_project_id + label: Enterprise Project ID(可选) + storage: config + config_key: enterprise_project_id + input_type: text + required: false +defaults: + region: "cn-north-4" + timeout: 60 + category: custom + product_version: "39" +notes: | + 华为云 WAF API 支持两种认证方式: + 1. AK/SK 签名认证(推荐):使用 SDK-HMAC-SHA256 签名, + 配置 ak / sk 字段即可。 + 2. Token 认证:直接填写有效的 X-Auth-Token,Token 有效期 24 小时。 + AK/SK 认证优先级高于 Token;若两者均未配置则报错。 + project_id 为必填项,可在控制台"我的凭证 → 项目列表"中获取。 diff --git a/.flocks/flockshub/plugins/tools/device/huaweicloud_waf_v39/_test.yaml b/.flocks/flockshub/plugins/tools/device/huaweicloud_waf_v39/_test.yaml new file mode 100644 index 00000000..1316a895 --- /dev/null +++ b/.flocks/flockshub/plugins/tools/device/huaweicloud_waf_v39/_test.yaml @@ -0,0 +1,65 @@ +schema_version: 1 +provider: huaweicloud_waf_api + +# Service-level connectivity probe. +# `host_list` is a lightweight read-only call with page=1,pagesize=1. +connectivity: + tool: hw_waf_host + params: + action: host_list + page: 1 + pagesize: 1 + +# Tool-level test samples shown in the WebUI ToolDetailDrawer drop-down. +fixtures: + hw_waf_host: + - label: "List cloud mode protected domains (page 1)" + label_cn: "查询云模式防护域名列表(第 1 页)" + tags: [smoke] + params: + action: host_list + page: 1 + pagesize: 10 + assert: + success: true + + - label: "List dedicated mode protected domains (page 1)" + label_cn: "查询独享模式防护域名列表(第 1 页)" + tags: [smoke] + params: + action: premium_host_list + page: 1 + pagesize: 10 + assert: + success: true + + hw_waf_policy: + - label: "List protection policies (page 1)" + label_cn: "查询防护策略列表(第 1 页)" + tags: [smoke] + params: + action: policy_list + page: 1 + pagesize: 10 + assert: + success: true + + hw_waf_event: + - label: "List attack events (last hour)" + label_cn: "查询近 1 小时攻击事件" + tags: [smoke] + params: + action: event_list + page: 1 + pagesize: 10 + assert: + success: true + + hw_waf_overview: + - label: "Query security statistics" + label_cn: "查询安全统计概览" + tags: [smoke] + params: + action: overview_statistics + assert: + success: true diff --git a/.flocks/flockshub/plugins/tools/device/huaweicloud_waf_v39/hw_waf.handler.py b/.flocks/flockshub/plugins/tools/device/huaweicloud_waf_v39/hw_waf.handler.py new file mode 100644 index 00000000..fd0e4409 --- /dev/null +++ b/.flocks/flockshub/plugins/tools/device/huaweicloud_waf_v39/hw_waf.handler.py @@ -0,0 +1,551 @@ +from __future__ import annotations + +import datetime +import hashlib +import hmac +import json +import os +from typing import Any, Optional +from urllib.parse import urlencode, quote + +import aiohttp + +from flocks.config.config_writer import ConfigWriter +from flocks.tool.registry import ToolContext, ToolResult + + +DEFAULT_REGION = "cn-north-4" +DEFAULT_TIMEOUT = 60 +SERVICE_ID = "huaweicloud_waf_api" +WAF_SERVICE_NAME = "waf" + + +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: + value = raw.get("verify_ssl") or raw.get("ssl_verify") + if value is None: + value = raw.get("custom_settings", {}).get("verify_ssl", True) + if isinstance(value, bool): + return value + if isinstance(value, str): + return value.strip().lower() in {"1", "true", "yes", "on"} + return bool(value) + + +class _WAFConfig: + def __init__( + self, + ak: Optional[str], + sk: Optional[str], + token: Optional[str], + region: str, + project_id: str, + enterprise_project_id: Optional[str], + timeout: int, + verify_ssl: bool, + ) -> None: + self.ak = ak + self.sk = sk + self.token = token + self.region = region + self.project_id = project_id + self.enterprise_project_id = enterprise_project_id + self.timeout = timeout + self.verify_ssl = verify_ssl + + @property + def endpoint(self) -> str: + return f"https://waf.{self.region}.myhuaweicloud.com" + + def use_aksk(self) -> bool: + return bool(self.ak and self.sk) + + +def _load_config(param_epid: Optional[str] = None) -> _WAFConfig: + raw = _service_config() + secret_manager = _get_secret_manager() + + ak = ( + _resolve_ref(raw.get("ak")) + or secret_manager.get("huaweicloud_waf_ak") + or os.getenv("HUAWEICLOUD_WAF_AK") + ) + sk = ( + _resolve_ref(raw.get("sk")) + or secret_manager.get("huaweicloud_waf_sk") + or os.getenv("HUAWEICLOUD_WAF_SK") + ) + token = ( + _resolve_ref(raw.get("token")) + or secret_manager.get("huaweicloud_waf_token") + or os.getenv("HUAWEICLOUD_WAF_TOKEN") + ) + region = _resolve_ref(raw.get("region")) or DEFAULT_REGION + project_id = _resolve_ref(raw.get("project_id")) or os.getenv("HUAWEICLOUD_PROJECT_ID", "") + if not project_id: + raise ValueError( + "Huawei Cloud WAF: project_id is required. " + "Configure it in the huaweicloud_waf_api service settings." + ) + if not ak and not token: + raise ValueError( + "Huawei Cloud WAF credentials not found. Configure ak/sk or token " + "in the huaweicloud_waf_api service settings." + ) + enterprise_project_id = ( + param_epid + or _resolve_ref(raw.get("enterprise_project_id")) + or os.getenv("HUAWEICLOUD_ENTERPRISE_PROJECT_ID") + ) + timeout = int(raw.get("timeout", DEFAULT_TIMEOUT)) + return _WAFConfig( + ak=ak, + sk=sk, + token=token, + region=region, + project_id=project_id, + enterprise_project_id=enterprise_project_id, + timeout=timeout, + verify_ssl=_resolve_verify_ssl(raw), + ) + + +# --------------------------------------------------------------------------- +# Huawei Cloud SDK-HMAC-SHA256 signing (AK/SK auth) +# Reference: https://support.huaweicloud.com/api-dew/dew_02_0008.html +# --------------------------------------------------------------------------- + +def _sha256_hex(data: bytes) -> str: + return hashlib.sha256(data).hexdigest() + + +def _hmac_sha256(key: bytes, msg: str) -> bytes: + return hmac.new(key, msg.encode("utf-8"), hashlib.sha256).digest() + + +def _build_aksk_headers( + ak: str, + sk: str, + method: str, + host: str, + uri: str, + query_string: str, + body: bytes, +) -> dict[str, str]: + """ + Build Huawei Cloud SDK-HMAC-SHA256 signed headers. + + Authorization = SDK-HMAC-SHA256 Access={ak}, + SignedHeaders={signed_headers}, Signature={signature} + """ + now = datetime.datetime.utcnow() + x_sdk_date = now.strftime("%Y%m%dT%H%M%SZ") + date_stamp = now.strftime("%Y%m%d") + + payload_hash = _sha256_hex(body) + headers_to_sign = { + "content-type": "application/json;charset=utf8", + "host": host, + "x-sdk-date": x_sdk_date, + } + signed_headers = ";".join(sorted(headers_to_sign.keys())) + canonical_headers = "".join( + f"{k}:{v}\n" for k, v in sorted(headers_to_sign.items()) + ) + canonical_uri = uri if uri else "/" + canonical_request = "\n".join([ + method.upper(), + canonical_uri, + query_string, + canonical_headers, + signed_headers, + payload_hash, + ]) + credential_scope = f"{date_stamp}/{WAF_SERVICE_NAME}/sdk_request" + string_to_sign = "\n".join([ + "SDK-HMAC-SHA256", + x_sdk_date, + credential_scope, + _sha256_hex(canonical_request.encode("utf-8")), + ]) + signing_key = _hmac_sha256( + _hmac_sha256( + _hmac_sha256( + _hmac_sha256( + ("SDK" + sk).encode("utf-8"), + date_stamp, + ), + WAF_SERVICE_NAME, + ), + "sdk_request", + ), + "sdk_signing", + ) + signature = hmac.new(signing_key, string_to_sign.encode("utf-8"), hashlib.sha256).hexdigest() + authorization = ( + f"SDK-HMAC-SHA256 Access={ak}, " + f"SignedHeaders={signed_headers}, " + f"Signature={signature}" + ) + return { + "Content-Type": "application/json;charset=utf8", + "X-Sdk-Date": x_sdk_date, + "Authorization": authorization, + } + + +async def _request( + cfg: _WAFConfig, + method: str, + path: str, + query: Optional[dict[str, Any]] = None, + body: Optional[dict[str, Any]] = None, +) -> ToolResult: + query = query or {} + if cfg.enterprise_project_id: + query.setdefault("enterprise_project_id", cfg.enterprise_project_id) + qs = urlencode({k: v for k, v in query.items() if v is not None}) + url = f"{cfg.endpoint}{path}" + if qs: + url = f"{url}?{qs}" + + body_bytes = json.dumps(body, ensure_ascii=False).encode("utf-8") if body else b"" + host = f"waf.{cfg.region}.myhuaweicloud.com" + + if cfg.use_aksk(): + headers = _build_aksk_headers( + cfg.ak, + cfg.sk, + method, + host, + path, + qs, + body_bytes, + ) + else: + headers = { + "Content-Type": "application/json;charset=utf8", + "X-Auth-Token": cfg.token, + } + + connector = aiohttp.TCPConnector(ssl=cfg.verify_ssl) + async with aiohttp.ClientSession(connector=connector) as session: + req_kwargs: dict[str, Any] = { + "headers": headers, + "timeout": aiohttp.ClientTimeout(total=cfg.timeout), + } + if body_bytes: + req_kwargs["data"] = body_bytes + async with session.request(method, url, **req_kwargs) as resp: + resp_text = await resp.text() + try: + resp_json = json.loads(resp_text) + except Exception: + resp_json = {"raw": resp_text} + if resp.status >= 400: + return ToolResult( + success=False, + data=resp_json, + error=f"HTTP {resp.status}: {resp_text[:300]}", + ) + return ToolResult(success=True, data=resp_json) + + +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 _page_query(params: dict[str, Any]) -> dict[str, Any]: + q: dict[str, Any] = {} + if "page" in params and params["page"] is not None: + q["page"] = params["page"] + if "pagesize" in params and params["pagesize"] is not None: + q["pagesize"] = params["pagesize"] + return q + + +# --------------------------------------------------------------------------- +# Tool handler functions +# --------------------------------------------------------------------------- + + +async def host(params: dict[str, Any], ctx: ToolContext) -> ToolResult: + cfg = _load_config(params.get("enterprise_project_id")) + pid = cfg.project_id + action = params.get("action", "") + + if action == "host_list": + q = _page_query(params) + if params.get("hostname"): + q["hostname"] = params["hostname"] + if params.get("policyname"): + q["policyname"] = params["policyname"] + return await _request(cfg, "GET", f"/v1/{pid}/waf/instance", query=q) + + if action == "host_show": + iid = params["instance_id"] + return await _request(cfg, "GET", f"/v1/{pid}/waf/instance/{iid}") + + if action == "host_create": + body = _pick(params, "hostname", "proxy", "server", "certificateid") + return await _request(cfg, "POST", f"/v1/{pid}/waf/instance", body=body) + + if action == "host_update": + iid = params["instance_id"] + body = _pick(params, "proxy", "server", "certificateid", "protect_status") + return await _request(cfg, "PATCH", f"/v1/{pid}/waf/instance/{iid}", body=body) + + if action == "host_delete": + iid = params["instance_id"] + return await _request(cfg, "DELETE", f"/v1/{pid}/waf/instance/{iid}") + + if action == "host_update_protect_status": + iid = params["instance_id"] + body = _pick(params, "protect_status") + return await _request(cfg, "PUT", f"/v1/{pid}/waf/instance/{iid}/protect-status", body=body) + + if action == "premium_host_list": + q = _page_query(params) + if params.get("hostname"): + q["hostname"] = params["hostname"] + if params.get("policyname"): + q["policyname"] = params["policyname"] + return await _request(cfg, "GET", f"/v1/{pid}/waf/premium-host", query=q) + + if action == "premium_host_show": + iid = params["instance_id"] + return await _request(cfg, "GET", f"/v1/{pid}/waf/premium-host/{iid}") + + if action == "premium_host_create": + body = _pick(params, "hostname", "proxy", "server", "web_tag", "certificateid") + return await _request(cfg, "POST", f"/v1/{pid}/waf/premium-host", body=body) + + if action == "premium_host_update": + iid = params["instance_id"] + body = _pick(params, "proxy", "server", "protect_status", "certificateid") + return await _request(cfg, "PATCH", f"/v1/{pid}/waf/premium-host/{iid}", body=body) + + if action == "premium_host_delete": + iid = params["instance_id"] + return await _request(cfg, "DELETE", f"/v1/{pid}/waf/premium-host/{iid}") + + if action == "composite_host_list": + q = _page_query(params) + if params.get("hostname"): + q["hostname"] = params["hostname"] + return await _request(cfg, "GET", f"/v1/{pid}/composite-waf/host", query=q) + + return ToolResult(success=False, error=f"Unknown action: {action}") + + +async def policy(params: dict[str, Any], ctx: ToolContext) -> ToolResult: + cfg = _load_config(params.get("enterprise_project_id")) + pid = cfg.project_id + action = params.get("action", "") + + if action == "policy_list": + q = _page_query(params) + if params.get("name"): + q["name"] = params["name"] + return await _request(cfg, "GET", f"/v1/{pid}/waf/policy", query=q) + + if action == "policy_show": + pol_id = params["policy_id"] + return await _request(cfg, "GET", f"/v1/{pid}/waf/policy/{pol_id}") + + if action == "policy_create": + body = _pick(params, "name", "level", "full_detection") + return await _request(cfg, "POST", f"/v1/{pid}/waf/policy", body=body) + + if action == "policy_update": + pol_id = params["policy_id"] + body = _pick(params, "name", "level", "full_detection", "options") + return await _request(cfg, "PATCH", f"/v1/{pid}/waf/policy/{pol_id}", body=body) + + if action == "policy_delete": + pol_id = params["policy_id"] + return await _request(cfg, "DELETE", f"/v1/{pid}/waf/policy/{pol_id}") + + if action == "policy_update_hosts": + pol_id = params["policy_id"] + body = _pick(params, "hosts") + return await _request(cfg, "PUT", f"/v1/{pid}/waf/policy/{pol_id}/hosts", body=body) + + if action == "cc_rule_list": + pol_id = params["policy_id"] + q = _page_query(params) + return await _request(cfg, "GET", f"/v1/{pid}/waf/policy/{pol_id}/cc", query=q) + + if action == "cc_rule_create": + pol_id = params["policy_id"] + body = _pick(params, "url", "limit_num", "limit_period", "lock_time", "tag_type", "action") + return await _request(cfg, "POST", f"/v1/{pid}/waf/policy/{pol_id}/cc", body=body) + + if action == "cc_rule_delete": + pol_id = params["policy_id"] + rule_id = params["rule_id"] + return await _request(cfg, "DELETE", f"/v1/{pid}/waf/policy/{pol_id}/cc/{rule_id}") + + if action == "custom_rule_list": + pol_id = params["policy_id"] + q = _page_query(params) + return await _request(cfg, "GET", f"/v1/{pid}/waf/policy/{pol_id}/custom", query=q) + + if action == "custom_rule_create": + pol_id = params["policy_id"] + body = _pick(params, "name", "conditions", "action", "priority", "description") + return await _request(cfg, "POST", f"/v1/{pid}/waf/policy/{pol_id}/custom", body=body) + + if action == "custom_rule_delete": + pol_id = params["policy_id"] + rule_id = params["rule_id"] + return await _request(cfg, "DELETE", f"/v1/{pid}/waf/policy/{pol_id}/custom/{rule_id}") + + if action == "whiteblackip_rule_list": + pol_id = params["policy_id"] + q = _page_query(params) + return await _request(cfg, "GET", f"/v1/{pid}/waf/policy/{pol_id}/whiteblackip", query=q) + + if action == "whiteblackip_rule_create": + pol_id = params["policy_id"] + body = _pick(params, "addr", "white", "description") + return await _request(cfg, "POST", f"/v1/{pid}/waf/policy/{pol_id}/whiteblackip", body=body) + + if action == "whiteblackip_rule_delete": + pol_id = params["policy_id"] + rule_id = params["rule_id"] + return await _request(cfg, "DELETE", f"/v1/{pid}/waf/policy/{pol_id}/whiteblackip/{rule_id}") + + if action == "geoip_rule_list": + pol_id = params["policy_id"] + q = _page_query(params) + return await _request(cfg, "GET", f"/v1/{pid}/waf/policy/{pol_id}/geoip", query=q) + + return ToolResult(success=False, error=f"Unknown action: {action}") + + +async def event(params: dict[str, Any], ctx: ToolContext) -> ToolResult: + cfg = _load_config(params.get("enterprise_project_id")) + pid = cfg.project_id + action = params.get("action", "") + + if action == "event_list": + q = _page_query(params) + for k in ("from", "to", "hosts", "attacks", "action"): + if params.get(k) is not None: + q[k] = params[k] + return await _request(cfg, "GET", f"/v1/{pid}/waf/event/attack/logs", query=q) + + if action == "event_show": + eid = params["eventid"] + return await _request(cfg, "GET", f"/v1/{pid}/waf/event/attack/logs/{eid}") + + if action == "event_log_download": + q: dict[str, Any] = {} + for k in ("from", "to"): + if params.get(k) is not None: + q[k] = params[k] + return await _request(cfg, "GET", f"/v1/{pid}/waf/event/attack/log/download", query=q) + + if action == "event_export_job": + body = {} + for k in ("from", "to", "hosts", "attacks", "action"): + if params.get(k) is not None: + body[k] = params[k] + return await _request(cfg, "POST", f"/v1/{pid}/waf/event/attack/log/job", body=body) + + if action == "threat_distribution": + q = {} + for k in ("from", "to", "hosts"): + if params.get(k) is not None: + q[k] = params[k] + return await _request(cfg, "GET", f"/v1/{pid}/waf/overviews/attack/types", query=q) + + if action == "top_url": + q = {} + for k in ("from", "to", "hosts", "top"): + if params.get(k) is not None: + q[k] = params[k] + return await _request(cfg, "GET", f"/v1/{pid}/waf/overviews/attack/top/url", query=q) + + if action == "top_source_ip": + q = {} + for k in ("from", "to", "hosts", "top"): + if params.get(k) is not None: + q[k] = params[k] + return await _request(cfg, "GET", f"/v1/{pid}/waf/overviews/attack/top/source", query=q) + + return ToolResult(success=False, error=f"Unknown action: {action}") + + +async def overview(params: dict[str, Any], ctx: ToolContext) -> ToolResult: + cfg = _load_config(params.get("enterprise_project_id")) + pid = cfg.project_id + action = params.get("action", "") + + def _time_query() -> dict[str, Any]: + q: dict[str, Any] = {} + for k in ("from", "to", "hosts"): + if params.get(k) is not None: + q[k] = params[k] + return q + + if action == "overview_statistics": + return await _request(cfg, "GET", f"/v1/{pid}/waf/overviews/statistics", query=_time_query()) + + if action == "overview_qps": + return await _request(cfg, "GET", f"/v1/{pid}/waf/overviews/statistics/qps", query=_time_query()) + + if action == "overview_bandwidth": + return await _request(cfg, "GET", f"/v1/{pid}/waf/overviews/statistics/bandwidth", query=_time_query()) + + if action == "overview_top_domains": + q = _time_query() + if params.get("top"): + q["top"] = params["top"] + return await _request(cfg, "GET", f"/v1/{pid}/waf/overviews/attack/top/host", query=q) + + if action == "overview_attack_types": + return await _request(cfg, "GET", f"/v1/{pid}/waf/overviews/attack/types", query=_time_query()) + + if action == "overview_top_ip": + q = _time_query() + if params.get("top"): + q["top"] = params["top"] + return await _request(cfg, "GET", f"/v1/{pid}/waf/overviews/attack/top/source", query=q) + + if action == "overview_top_url": + q = _time_query() + if params.get("top"): + q["top"] = params["top"] + return await _request(cfg, "GET", f"/v1/{pid}/waf/overviews/attack/top/url", query=q) + + if action == "overview_response_code": + return await _request(cfg, "GET", f"/v1/{pid}/waf/overviews/statistics/response_code", query=_time_query()) + + if action == "console_config": + return await _request(cfg, "GET", f"/v1/{pid}/waf/config/console") + + return ToolResult(success=False, error=f"Unknown action: {action}") diff --git a/.flocks/flockshub/plugins/tools/device/huaweicloud_waf_v39/hw_waf_event.yaml b/.flocks/flockshub/plugins/tools/device/huaweicloud_waf_v39/hw_waf_event.yaml new file mode 100644 index 00000000..0c6c9971 --- /dev/null +++ b/.flocks/flockshub/plugins/tools/device/huaweicloud_waf_v39/hw_waf_event.yaml @@ -0,0 +1,115 @@ +name: hw_waf_event +description: > + Huawei Cloud WAF protection event management tool. Use the `action` + parameter to query attack events, export event logs, and analyze threat + distributions. +description_cn: > + 华为云 WAF 防护事件管理工具。通过 `action` 参数查询攻击事件列表、事件详情、 + 攻击分布分析和事件日志导出等接口。 +category: custom +enabled: true +requires_confirmation: true +provider: huaweicloud_waf_api +inputSchema: + type: object + properties: + action: + type: string + description: | + 防护事件管理动作名,可选值: + - event_list + 用途: 查询攻击事件列表(分页) + 必填: 无 + 常用: `from`、`to`、`hosts`、`attacks`、`action`、`page`、`pagesize` + 风险提示: 只读查询接口;建议传 from/to 缩小范围 + 是否任务型: 否 + - event_show + 用途: 查询指定攻击事件详情 + 必填: `eventid` + 常用: `eventid` + 风险提示: 只读查询接口 + 是否任务型: 否 + - event_log_download + 用途: 查询事件日志下载链接(按日期) + 必填: 无 + 常用: `from`、`to` + 风险提示: 只读查询接口;返回 URL 有效期较短 + 是否任务型: 否 + - event_export_job + 用途: 下发自定义导出攻击事件的异步任务 + 必填: `from`、`to` + 常用: `from`、`to`、`hosts`、`attacks`、`action` + 风险提示: 写操作(下发异步任务);任务完成后可通过 `event_log_download` 获取结果 + 是否任务型: 是 + - threat_distribution + 用途: 查询攻击事件分布类型(Top 攻击类型) + 必填: `from`、`to` + 常用: `from`、`to`、`hosts` + 风险提示: 只读查询接口 + 是否任务型: 否 + - top_url + 用途: 查询事件日志中的 Top 被攻击 URL + 必填: `from`、`to` + 常用: `from`、`to`、`hosts`、`top` + 风险提示: 只读查询接口 + 是否任务型: 否 + - top_source_ip + 用途: 查询 Top 攻击源 IP + 必填: `from`、`to` + 常用: `from`、`to`、`hosts`、`top` + 风险提示: 只读查询接口 + 是否任务型: 否 + enum: + - event_list + - event_show + - event_log_download + - event_export_job + - threat_distribution + - top_url + - top_source_ip + eventid: + type: string + description: 攻击事件 ID + from: + type: integer + description: > + 查询开始时间,Unix 毫秒级时间戳。 + 可使用 Python datetime 动态计算,例如: + int(datetime.now().timestamp() * 1000) - 3600000(最近 1 小时) + to: + type: integer + description: 查询结束时间,Unix 毫秒级时间戳,必须大于 from + hosts: + type: array + items: + type: string + description: 防护域名 ID 列表(过滤用) + attacks: + type: array + items: + type: string + description: 攻击类型列表(如 `sqli`、`xss`、`cmdi`、`cc` 等) + action: + type: string + description: 防护动作筛选,`block`(拦截)或 `log`(仅记录) + top: + type: integer + description: 返回 Top N 条数,默认 5 + default: 5 + page: + type: integer + description: 页码,从 1 开始 + default: 1 + pagesize: + type: integer + description: 每页数量,最大 100 + default: 10 + enterprise_project_id: + type: string + description: 企业项目 ID(可选) + required: + - action +handler: + type: script + script_file: hw_waf.handler.py + function: event diff --git a/.flocks/flockshub/plugins/tools/device/huaweicloud_waf_v39/hw_waf_host.yaml b/.flocks/flockshub/plugins/tools/device/huaweicloud_waf_v39/hw_waf_host.yaml new file mode 100644 index 00000000..620fa7a3 --- /dev/null +++ b/.flocks/flockshub/plugins/tools/device/huaweicloud_waf_v39/hw_waf_host.yaml @@ -0,0 +1,148 @@ +name: hw_waf_host +description: > + Huawei Cloud WAF protected domain management tool. Use the `action` + parameter to query, create, update, or delete cloud mode and dedicated + mode protected domains. +description_cn: > + 华为云 WAF 防护域名管理工具。通过 `action` 参数调用云模式和独享模式防护域名 + 的查询、创建、更新和删除等接口。 + 请在 WAF 服务配置中填写 AK/SK(或 Token)、Region 和 Project ID。 +category: custom +enabled: true +requires_confirmation: true +provider: huaweicloud_waf_api +inputSchema: + type: object + properties: + action: + type: string + description: | + 防护域名管理动作名,可选值: + - host_list + 用途: 查询云模式防护域名列表 + 必填: 无 + 常用: `page`、`pagesize`、`hostname`、`policyname` + 风险提示: 只读查询接口 + 是否任务型: 否 + - host_show + 用途: 根据域名 ID 查询云模式防护域名详情 + 必填: `instance_id` + 常用: `instance_id` + 风险提示: 只读查询接口 + 是否任务型: 否 + - host_create + 用途: 创建云模式防护域名 + 必填: `hostname`、`proxy`、`server` + 常用: `hostname`、`proxy`、`server`、`certificateid` + 风险提示: 写操作;会新增防护域名接入 + 是否任务型: 否 + - host_update + 用途: 更新云模式防护域名配置 + 必填: `instance_id` + 常用: `instance_id`、`proxy`、`server`、`certificateid`、`protect_status` + 风险提示: 写操作;会修改域名防护配置 + 是否任务型: 否 + - host_delete + 用途: 删除云模式防护域名 + 必填: `instance_id` + 常用: `instance_id` + 风险提示: 高风险写操作;删除后防护立即失效 + 是否任务型: 否 + - host_update_protect_status + 用途: 修改域名防护状态(开启/关闭/观察) + 必填: `instance_id`、`protect_status` + 常用: `instance_id`、`protect_status` + 风险提示: 写操作;会影响域名流量防护状态 + 是否任务型: 否 + - premium_host_list + 用途: 查询独享模式防护域名列表 + 必填: 无 + 常用: `page`、`pagesize`、`hostname`、`policyname` + 风险提示: 只读查询接口 + 是否任务型: 否 + - premium_host_show + 用途: 查看独享模式域名配置详情 + 必填: `instance_id` + 常用: `instance_id` + 风险提示: 只读查询接口 + 是否任务型: 否 + - premium_host_create + 用途: 创建独享模式防护域名 + 必填: `hostname`、`proxy`、`server`、`web_tag` + 常用: `hostname`、`proxy`、`server`、`web_tag`、`certificateid` + 风险提示: 写操作;会新增独享模式域名接入 + 是否任务型: 否 + - premium_host_update + 用途: 修改独享模式域名配置 + 必填: `instance_id` + 常用: `instance_id`、`proxy`、`server`、`protect_status` + 风险提示: 写操作;会修改独享域名防护配置 + 是否任务型: 否 + - premium_host_delete + 用途: 删除独享模式防护域名 + 必填: `instance_id` + 常用: `instance_id` + 风险提示: 高风险写操作;删除后防护立即失效 + 是否任务型: 否 + - composite_host_list + 用途: 查询全部(云模式+独享模式)防护域名列表 + 必填: 无 + 常用: `page`、`pagesize`、`hostname` + 风险提示: 只读查询接口 + 是否任务型: 否 + enum: + - host_list + - host_show + - host_create + - host_update + - host_delete + - host_update_protect_status + - premium_host_list + - premium_host_show + - premium_host_create + - premium_host_update + - premium_host_delete + - composite_host_list + instance_id: + type: string + description: 防护域名 ID + hostname: + type: string + description: 域名(查询时为模糊匹配关键词,创建时为精确域名) + policyname: + type: string + description: 防护策略名称(查询过滤用) + proxy: + type: boolean + description: 是否使用代理(true/false) + server: + type: array + items: + type: object + description: 源站服务器配置列表,每项含 `address`、`port`、`type`、`weight` 等字段 + certificateid: + type: string + description: 证书 ID(HTTPS 域名必填) + protect_status: + type: integer + description: 防护状态,`0` 关闭防护、`1` 开启防护、`-1` 观察模式 + web_tag: + type: string + description: 独享模式域名标签(创建独享域名时必填) + page: + type: integer + description: 页码,从 1 开始 + default: 1 + pagesize: + type: integer + description: 每页数量 + default: 10 + enterprise_project_id: + type: string + description: 企业项目 ID(可选,覆盖配置中的默认值) + required: + - action +handler: + type: script + script_file: hw_waf.handler.py + function: host diff --git a/.flocks/flockshub/plugins/tools/device/huaweicloud_waf_v39/hw_waf_overview.yaml b/.flocks/flockshub/plugins/tools/device/huaweicloud_waf_v39/hw_waf_overview.yaml new file mode 100644 index 00000000..ba43e8b2 --- /dev/null +++ b/.flocks/flockshub/plugins/tools/device/huaweicloud_waf_v39/hw_waf_overview.yaml @@ -0,0 +1,110 @@ +name: hw_waf_overview +description: > + Huawei Cloud WAF security overview and statistics tool. Use the `action` + parameter to query QPS data, bandwidth statistics, attack counts, top + attacked domains, and security report subscriptions. +description_cn: > + 华为云 WAF 安全总览与统计工具。通过 `action` 参数查询 QPS 数据、带宽统计、 + 攻击防护次数、Top 被攻击域名,以及安全报告订阅管理等接口。 +category: custom +enabled: true +requires_confirmation: true +provider: huaweicloud_waf_api +inputSchema: + type: object + properties: + action: + type: string + description: | + 安全总览动作名,可选值: + - overview_statistics + 用途: 查询安全总览请求与攻击数量汇总 + 必填: `from`、`to` + 常用: `from`、`to`、`hosts` + 风险提示: 只读查询接口 + 是否任务型: 否 + - overview_qps + 用途: 查询 QPS 时序数据(每 5 分钟一个数据点) + 必填: `from`、`to` + 常用: `from`、`to`、`hosts` + 风险提示: 只读查询接口 + 是否任务型: 否 + - overview_bandwidth + 用途: 查询带宽时序数据 + 必填: `from`、`to` + 常用: `from`、`to`、`hosts` + 风险提示: 只读查询接口 + 是否任务型: 否 + - overview_top_domains + 用途: 查询 Top 受攻击域名排行 + 必填: `from`、`to` + 常用: `from`、`to`、`top` + 风险提示: 只读查询接口 + 是否任务型: 否 + - overview_attack_types + 用途: 查询攻击类型分布 + 必填: `from`、`to` + 常用: `from`、`to`、`hosts` + 风险提示: 只读查询接口 + 是否任务型: 否 + - overview_top_ip + 用途: 查询 Top 攻击源 IP + 必填: `from`、`to` + 常用: `from`、`to`、`hosts`、`top` + 风险提示: 只读查询接口 + 是否任务型: 否 + - overview_top_url + 用途: 查询 Top 被攻击 URL + 必填: `from`、`to` + 常用: `from`、`to`、`hosts`、`top` + 风险提示: 只读查询接口 + 是否任务型: 否 + - overview_response_code + 用途: 查询响应码时序统计数据 + 必填: `from`、`to` + 常用: `from`、`to`、`hosts` + 风险提示: 只读查询接口 + 是否任务型: 否 + - console_config + 用途: 查询当前局点支持的 WAF 特性配置 + 必填: 无 + 常用: 无 + 风险提示: 只读查询接口 + 是否任务型: 否 + enum: + - overview_statistics + - overview_qps + - overview_bandwidth + - overview_top_domains + - overview_attack_types + - overview_top_ip + - overview_top_url + - overview_response_code + - console_config + from: + type: integer + description: > + 查询开始时间,Unix 毫秒级时间戳。 + 可使用 Python datetime 动态计算: + int(datetime.now().timestamp() * 1000) - 86400000(最近 24 小时) + to: + type: integer + description: 查询结束时间,Unix 毫秒级时间戳,必须大于 from + hosts: + type: array + items: + type: string + description: 防护域名 ID 列表(过滤用,不传则汇总所有域名) + top: + type: integer + description: 返回 Top N 条数,默认 5 + default: 5 + enterprise_project_id: + type: string + description: 企业项目 ID(可选) + required: + - action +handler: + type: script + script_file: hw_waf.handler.py + function: overview diff --git a/.flocks/flockshub/plugins/tools/device/huaweicloud_waf_v39/hw_waf_policy.yaml b/.flocks/flockshub/plugins/tools/device/huaweicloud_waf_v39/hw_waf_policy.yaml new file mode 100644 index 00000000..fb04016f --- /dev/null +++ b/.flocks/flockshub/plugins/tools/device/huaweicloud_waf_v39/hw_waf_policy.yaml @@ -0,0 +1,207 @@ +name: hw_waf_policy +description: > + Huawei Cloud WAF protection policy management tool. Use the `action` + parameter to query, create, update, or delete protection policies, and + manage policy rules (CC rules, custom rules, blacklist/whitelist, etc.). +description_cn: > + 华为云 WAF 防护策略管理工具。通过 `action` 参数调用防护策略的查询、创建、 + 更新和删除接口,以及 CC 规则、精准防护规则、黑白名单、地理位置控制等规则管理接口。 +category: custom +enabled: true +requires_confirmation: true +provider: huaweicloud_waf_api +inputSchema: + type: object + properties: + action: + type: string + description: | + 防护策略管理动作名,可选值: + - policy_list + 用途: 查询防护策略列表 + 必填: 无 + 常用: `page`、`pagesize`、`name` + 风险提示: 只读查询接口 + 是否任务型: 否 + - policy_show + 用途: 根据 ID 查询防护策略详情 + 必填: `policy_id` + 常用: `policy_id` + 风险提示: 只读查询接口 + 是否任务型: 否 + - policy_create + 用途: 创建防护策略 + 必填: `name` + 常用: `name`、`level`、`full_detection` + 风险提示: 写操作;会新增防护策略 + 是否任务型: 否 + - policy_update + 用途: 更新防护策略 + 必填: `policy_id` + 常用: `policy_id`、`name`、`level`、`full_detection`、`options` + 风险提示: 写操作;会修改防护策略参数 + 是否任务型: 否 + - policy_delete + 用途: 删除防护策略 + 必填: `policy_id` + 常用: `policy_id` + 风险提示: 高风险写操作;删除后绑定该策略的域名需重新绑定 + 是否任务型: 否 + - policy_update_hosts + 用途: 更新防护策略绑定的域名列表 + 必填: `policy_id`、`hosts` + 常用: `policy_id`、`hosts` + 风险提示: 写操作;会变更策略与域名的绑定关系 + 是否任务型: 否 + - cc_rule_list + 用途: 查询 CC 防护规则列表 + 必填: `policy_id` + 常用: `policy_id`、`page`、`pagesize` + 风险提示: 只读查询接口 + 是否任务型: 否 + - cc_rule_create + 用途: 创建 CC 防护规则 + 必填: `policy_id`、`url`、`limit_num`、`limit_period`、`lock_time`、`tag_type`、`action` + 常用: `policy_id`、`url`、`limit_num`、`limit_period`、`lock_time`、`tag_type`、`action` + 风险提示: 写操作;会新增 CC 规则 + 是否任务型: 否 + - cc_rule_delete + 用途: 删除 CC 防护规则 + 必填: `policy_id`、`rule_id` + 常用: `policy_id`、`rule_id` + 风险提示: 写操作;会删除 CC 规则 + 是否任务型: 否 + - custom_rule_list + 用途: 查询精准防护规则列表 + 必填: `policy_id` + 常用: `policy_id`、`page`、`pagesize` + 风险提示: 只读查询接口 + 是否任务型: 否 + - custom_rule_create + 用途: 创建精准防护规则 + 必填: `policy_id`、`name`、`conditions`、`action` + 常用: `policy_id`、`name`、`conditions`、`action`、`priority` + 风险提示: 写操作;会新增精准防护规则 + 是否任务型: 否 + - custom_rule_delete + 用途: 删除精准防护规则 + 必填: `policy_id`、`rule_id` + 常用: `policy_id`、`rule_id` + 风险提示: 写操作;会删除精准防护规则 + 是否任务型: 否 + - whiteblackip_rule_list + 用途: 查询黑白名单规则列表 + 必填: `policy_id` + 常用: `policy_id`、`page`、`pagesize` + 风险提示: 只读查询接口 + 是否任务型: 否 + - whiteblackip_rule_create + 用途: 创建黑白名单规则(IP/IP 段 允许/拦截) + 必填: `policy_id`、`addr`、`white` + 常用: `policy_id`、`addr`、`white`、`description` + 风险提示: 写操作;会新增 IP 黑白名单规则,影响访问控制 + 是否任务型: 否 + - whiteblackip_rule_delete + 用途: 删除黑白名单规则 + 必填: `policy_id`、`rule_id` + 常用: `policy_id`、`rule_id` + 风险提示: 写操作;会删除 IP 黑白名单规则 + 是否任务型: 否 + - geoip_rule_list + 用途: 查询地理位置访问控制规则列表 + 必填: `policy_id` + 常用: `policy_id`、`page`、`pagesize` + 风险提示: 只读查询接口 + 是否任务型: 否 + enum: + - policy_list + - policy_show + - policy_create + - policy_update + - policy_delete + - policy_update_hosts + - cc_rule_list + - cc_rule_create + - cc_rule_delete + - custom_rule_list + - custom_rule_create + - custom_rule_delete + - whiteblackip_rule_list + - whiteblackip_rule_create + - whiteblackip_rule_delete + - geoip_rule_list + policy_id: + type: string + description: 防护策略 ID + rule_id: + type: string + description: 规则 ID + name: + type: string + description: 策略名称(查询时为模糊匹配关键词,创建时为精确名称) + level: + type: integer + description: 防护等级,`1` 宽松、`2` 中等(默认)、`3` 严格 + full_detection: + type: boolean + description: 是否开启全检测模式 + options: + type: object + description: 防护开关选项对象(如 webattack、cc、custom 等开关) + hosts: + type: array + items: + type: object + description: 域名列表,每项含 `id`(域名 ID)字段 + url: + type: string + description: CC 规则匹配的 URL 路径 + limit_num: + type: integer + description: CC 规则限制请求数 + limit_period: + type: integer + description: CC 规则统计时间窗(秒) + lock_time: + type: integer + description: CC 规则封禁时长(秒) + tag_type: + type: string + description: CC 规则标签类型,如 `ip`、`cookie`、`header` 等 + action: + type: object + description: 规则匹配后的动作,含 `category`(`block`/`pass`/`log`)等字段 + conditions: + type: array + items: + type: object + description: 精准防护规则条件列表 + priority: + type: integer + description: 精准防护规则优先级(数字越小优先级越高) + addr: + type: string + description: 黑白名单 IP 地址或 CIDR 段 + white: + type: integer + description: 白名单标识,`0` 黑名单(拦截)、`1` 白名单(放行) + description: + type: string + description: 规则备注描述 + page: + type: integer + description: 页码,从 1 开始 + default: 1 + pagesize: + type: integer + description: 每页数量 + default: 10 + enterprise_project_id: + type: string + description: 企业项目 ID(可选) + required: + - action +handler: + type: script + script_file: hw_waf.handler.py + function: policy diff --git a/.flocks/flockshub/plugins/tools/device/huorong_edr_v1_0/_provider.yaml b/.flocks/flockshub/plugins/tools/device/huorong_edr_v1_0/_provider.yaml new file mode 100644 index 00000000..0a49d2bd --- /dev/null +++ b/.flocks/flockshub/plugins/tools/device/huorong_edr_v1_0/_provider.yaml @@ -0,0 +1,50 @@ +name: huorong_edr +vendor: huorong +service_id: huorong_api +version: "1.0" +integration_type: device +description: > + Huorong EDR (Endpoint Detection & Response) API service. Configure the + Access Key ID, Secret Key, and Base URL in the credentials form. + Base URL should point to your Huorong console (e.g., `http://192.168.1.100:801`). +description_cn: > + 火绒终端安全管理系统 API 服务。在配置页分别填写 Access Key ID(secret_id)、 + Secret Key(secret_key)和 Base URL(控制台地址,例如 `http://192.168.1.100:801`)。 +auth: + type: custom + secret: huorong_secret_id + secret_secret: huorong_secret_key +credential_fields: + - key: secret_id + label: Access Key ID + storage: secret + config_key: secretId + secret_id: huorong_secret_id + input_type: password + required: true + - key: secret_key + label: Secret Key + storage: secret + config_key: secretKey + secret_id: huorong_secret_key + input_type: password + required: true + - key: base_url + label: Base URL + storage: config + config_key: base_url + input_type: url + default: "http://localhost:801" +defaults: + base_url: "http://localhost:801" + timeout: 30 + category: custom + product_version: "1.0" +notes: | + 火绒 API 使用 HMAC-SHA1 请求签名认证。 + Authorization 请求头格式:HRESS{secret_id}:{expires}:{url_encoded_sign} + 签名字符串:{secret_id}\n{expires}\n{METHOD}\n{content_md5}\n{canonical_resource} + - expires:Unix 秒级时间戳 + 3600 + - content_md5:base64(md5(request_body)) + - canonical_resource:请求路径去掉开头的 "/" + 也支持 URL 查询参数方式:?ak={secret_id}&expires={expires}&sign={sign} diff --git a/.flocks/flockshub/plugins/tools/device/huorong_edr_v1_0/_test.yaml b/.flocks/flockshub/plugins/tools/device/huorong_edr_v1_0/_test.yaml new file mode 100644 index 00000000..1747864f --- /dev/null +++ b/.flocks/flockshub/plugins/tools/device/huorong_edr_v1_0/_test.yaml @@ -0,0 +1,49 @@ +schema_version: 1 +provider: huorong_api + +# Service-level connectivity probe. +# `group_list` is a lightweight read-only call with no required params. +connectivity: + tool: huorong_group + params: + action: group_list + +# Tool-level test samples shown in the WebUI ToolDetailDrawer drop-down. +fixtures: + huorong_group: + - label: "List all groups" + label_cn: "获取全部分组列表" + tags: [smoke] + params: + action: group_list + assert: + success: true + + huorong_clnts: + - label: "List online endpoints" + label_cn: "查询上线终端列表" + tags: [smoke] + params: + action: clnts_online + offset: 0 + assert: + success: true + + - label: "List endpoint details (page 1)" + label_cn: "查询终端详情(第 1 页)" + tags: [smoke] + params: + action: clnts_list + offset: 0 + assert: + success: true + + huorong_task: + - label: "Create virus scan task" + label_cn: "创建查杀扫描任务" + tags: [smoke] + params: + action: task_create + offset: 0 + assert: + success: true diff --git a/.flocks/flockshub/plugins/tools/device/huorong_edr_v1_0/huorong.handler.py b/.flocks/flockshub/plugins/tools/device/huorong_edr_v1_0/huorong.handler.py new file mode 100644 index 00000000..3168dfdb --- /dev/null +++ b/.flocks/flockshub/plugins/tools/device/huorong_edr_v1_0/huorong.handler.py @@ -0,0 +1,243 @@ +from __future__ import annotations + +import base64 +import hashlib +import hmac +import json +import os +import time +import urllib.parse as up +from typing import Any, Optional + +import aiohttp + +from flocks.config.config_writer import ConfigWriter +from flocks.tool.registry import ToolContext, ToolResult + + +DEFAULT_BASE_URL = "http://localhost:801" +DEFAULT_TIMEOUT = 30 +SERVICE_ID = "huorong_api" + + +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: + value = raw.get("verify_ssl") + if value is None: + value = raw.get("ssl_verify") + if value is None: + custom_settings = raw.get("custom_settings", {}) + if isinstance(custom_settings, dict): + value = custom_settings.get("verify_ssl", False) + if isinstance(value, bool): + return value + if isinstance(value, str): + return value.strip().lower() in {"1", "true", "yes", "on"} + return bool(value) + + +def _resolve_runtime_config() -> tuple[str, int, str, str, bool]: + raw = _service_config() + base_url = ( + _resolve_ref(raw.get("base_url")) + or _resolve_ref(raw.get("baseUrl")) + or DEFAULT_BASE_URL + ).rstrip("/") + timeout = raw.get("timeout", DEFAULT_TIMEOUT) + try: + timeout = int(timeout) + except (TypeError, ValueError): + timeout = DEFAULT_TIMEOUT + + secret_manager = _get_secret_manager() + + secret_id = ( + _resolve_ref(raw.get("secretId")) + or _resolve_ref(raw.get("secret_id")) + or secret_manager.get("huorong_secret_id") + or os.getenv("HUORONG_SECRET_ID") + ) + secret_key = ( + _resolve_ref(raw.get("secretKey")) + or _resolve_ref(raw.get("secret_key")) + or secret_manager.get("huorong_secret_key") + or os.getenv("HUORONG_SECRET_KEY") + ) + if not secret_id or not secret_key: + raise ValueError( + "Huorong API credentials not found. Configure secretId and secretKey " + "in the huorong_api service settings." + ) + return base_url, timeout, secret_id, secret_key, _resolve_verify_ssl(raw) + + +def _build_auth_header( + secret_id: str, + secret_key: str, + method: str, + path: str, + body: str, +) -> str: + """ + Build Huorong HMAC-SHA1 Authorization header. + + Authorization = HRESS{secret_id}:{expires}:{url_encoded_sign} + StringToSign = {secret_id}\\n{expires}\\n{method}\\n{content_md5}\\n{canonical_resource} + content_md5 = base64(md5(body)) + canonical_resource = path without leading "/" + sign = url_encode(base64(hmac-sha1(secret_key, string_to_sign))) + """ + expires = int(time.time()) + 3600 + body_bytes = body.encode("utf-8") if isinstance(body, str) else body + content_md5 = base64.b64encode( + hashlib.md5(body_bytes).digest() + ).decode("utf-8") + canonical_resource = path.lstrip("/") + string_to_sign = f"{secret_id}\n{expires}\n{method}\n{content_md5}\n{canonical_resource}" + sign_bytes = hmac.new( + secret_key.encode("utf-8"), + string_to_sign.encode("utf-8"), + "sha1", + ).digest() + sign = up.quote(base64.b64encode(sign_bytes).decode("utf-8")) + return f"HRESS{secret_id}:{expires}:{sign}" + + +async def _post( + base_url: str, + path: str, + body: dict[str, Any], + secret_id: str, + secret_key: str, + timeout: int, + verify_ssl: bool, +) -> ToolResult: + body_str = json.dumps(body, ensure_ascii=False) + auth_header = _build_auth_header(secret_id, secret_key, "POST", path, body_str) + url = f"{base_url}{path}" + connector = aiohttp.TCPConnector(ssl=verify_ssl) + async with aiohttp.ClientSession(connector=connector) as session: + async with session.post( + url, + data=body_str.encode("utf-8"), + headers={ + "Content-Type": "application/json;charset=UTF-8", + "Authorization": auth_header, + }, + timeout=aiohttp.ClientTimeout(total=timeout), + ) as resp: + resp_text = await resp.text() + try: + resp_json = json.loads(resp_text) + except Exception: + resp_json = {"raw": resp_text} + if resp.status >= 400: + return ToolResult( + success=False, + data=resp_json, + error=f"HTTP {resp.status}: {resp_text[:200]}", + ) + return ToolResult(success=True, data=resp_json) + + +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} + + +# --------------------------------------------------------------------------- +# Public handler functions (one per tool YAML's `function` field) +# --------------------------------------------------------------------------- + + +async def group(params: dict[str, Any], ctx: ToolContext) -> ToolResult: + base_url, timeout, secret_id, secret_key, verify_ssl = _resolve_runtime_config() + action = params.get("action", "") + + if action == "group_list": + return await _post(base_url, "/api/group/_list", {}, secret_id, secret_key, timeout, verify_ssl) + + if action == "group_create": + body = _pick(params, "group_name", "parent_group") + body.setdefault("parent_group", 0) + return await _post(base_url, "/api/group/_create", body, secret_id, secret_key, timeout, verify_ssl) + + if action == "group_rename": + body = _pick(params, "group_id", "group_name") + return await _post(base_url, "/api/group/_rename", body, secret_id, secret_key, timeout, verify_ssl) + + if action == "group_delete": + body = _pick(params, "group_id") + return await _post(base_url, "/api/group/_delete", body, secret_id, secret_key, timeout, verify_ssl) + + return ToolResult(success=False, error=f"Unknown action: {action}") + + +async def clnts(params: dict[str, Any], ctx: ToolContext) -> ToolResult: + base_url, timeout, secret_id, secret_key, verify_ssl = _resolve_runtime_config() + action = params.get("action", "") + + if action == "clnts_online": + body = _pick(params, "offset") + body.setdefault("offset", 0) + return await _post(base_url, "/api/clnts/_online", body, secret_id, secret_key, timeout, verify_ssl) + + if action == "clnts_list": + body = _pick(params, "offset") + body.setdefault("offset", 0) + return await _post(base_url, "/api/clnts/_list", body, secret_id, secret_key, timeout, verify_ssl) + + if action == "clnts_info": + body = _pick(params, "clients") + return await _post(base_url, "/api/clnts/_info", body, secret_id, secret_key, timeout, verify_ssl) + + if action == "clnts_info2": + body = _pick(params, "clients") + return await _post(base_url, "/api/clnts/_info2", body, secret_id, secret_key, timeout, verify_ssl) + + if action == "clnts_rename": + body = _pick(params, "client_id", "client_name") + return await _post(base_url, "/api/clnts/_rename", body, secret_id, secret_key, timeout, verify_ssl) + + if action == "clnts_group": + body = _pick(params, "group_id", "clients") + return await _post(base_url, "/api/clnts/_group", body, secret_id, secret_key, timeout, verify_ssl) + + if action == "clnts_leak": + body = _pick(params, "clients") + return await _post(base_url, "/api/clnts/_leak", body, secret_id, secret_key, timeout, verify_ssl) + + return ToolResult(success=False, error=f"Unknown action: {action}") + + +async def task(params: dict[str, Any], ctx: ToolContext) -> ToolResult: + base_url, timeout, secret_id, secret_key, verify_ssl = _resolve_runtime_config() + action = params.get("action", "") + + if action == "task_create": + body = _pick(params, "offset") + body.setdefault("offset", 0) + return await _post(base_url, "/api/task/_create", body, secret_id, secret_key, timeout, verify_ssl) + + return ToolResult(success=False, error=f"Unknown action: {action}") diff --git a/.flocks/flockshub/plugins/tools/device/huorong_edr_v1_0/huorong_clnts.yaml b/.flocks/flockshub/plugins/tools/device/huorong_edr_v1_0/huorong_clnts.yaml new file mode 100644 index 00000000..8b883853 --- /dev/null +++ b/.flocks/flockshub/plugins/tools/device/huorong_edr_v1_0/huorong_clnts.yaml @@ -0,0 +1,94 @@ +name: huorong_clnts +description: > + Huorong EDR endpoint (client) management tool. Use the `action` parameter + to query online status, list details, rename, move groups, or check for + vulnerabilities. +description_cn: > + 火绒终端(客户端)管理工具。通过 `action` 参数调用终端上线查询、详情列表、 + 重命名、分组变更和高危漏洞查询等接口。 + 请在火绒服务配置中分别填写 Access Key ID、Secret Key 和 Base URL。 +category: custom +enabled: true +requires_confirmation: true +provider: huorong_api +inputSchema: + type: object + properties: + action: + type: string + description: | + 终端管理动作名,可选值: + - clnts_online + 用途: 查询当前在线终端列表(分页) + 必填: 无 + 常用: `offset` + 风险提示: 只读查询接口 + 是否任务型: 否 + - clnts_list + 用途: 查询终端详情列表(分页) + 必填: 无 + 常用: `offset` + 风险提示: 只读查询接口 + 是否任务型: 否 + - clnts_info + 用途: 查询指定终端详细信息(v1) + 必填: `clients` + 常用: `clients` + 风险提示: 只读查询接口;`clients` 为终端 client_id 列表 + 是否任务型: 否 + - clnts_info2 + 用途: 查询指定终端详细信息(v2,字段更丰富) + 必填: `clients` + 常用: `clients` + 风险提示: 只读查询接口;`clients` 为终端 client_id 列表 + 是否任务型: 否 + - clnts_rename + 用途: 修改终端名称 + 必填: `client_id`、`client_name` + 常用: `client_id`、`client_name` + 风险提示: 写操作;会直接修改终端显示名称 + 是否任务型: 否 + - clnts_group + 用途: 修改终端所属分组 + 必填: `group_id`、`clients` + 常用: `group_id`、`clients` + 风险提示: 写操作;会将终端移入指定分组 + 是否任务型: 否 + - clnts_leak + 用途: 查询存在高危漏洞未修复的终端 + 必填: `clients` + 常用: `clients` + 风险提示: 只读查询接口;返回存在高危漏洞的终端信息 + 是否任务型: 否 + enum: + - clnts_online + - clnts_list + - clnts_info + - clnts_info2 + - clnts_rename + - clnts_group + - clnts_leak + offset: + type: integer + description: 分页偏移量,从 0 开始 + default: 0 + clients: + type: array + items: + type: string + description: 终端 client_id 列表(40 字符十六进制字符串) + client_id: + type: string + description: 单个终端 client_id(40 字符十六进制字符串) + client_name: + type: string + description: 终端新名称 + group_id: + type: integer + description: 目标分组 ID + required: + - action +handler: + type: script + script_file: huorong.handler.py + function: clnts diff --git a/.flocks/flockshub/plugins/tools/device/huorong_edr_v1_0/huorong_group.yaml b/.flocks/flockshub/plugins/tools/device/huorong_edr_v1_0/huorong_group.yaml new file mode 100644 index 00000000..6c0c2f72 --- /dev/null +++ b/.flocks/flockshub/plugins/tools/device/huorong_edr_v1_0/huorong_group.yaml @@ -0,0 +1,63 @@ +name: huorong_group +description: > + Huorong EDR group management tool. Use the `action` parameter to list, + create, rename, or delete endpoint groups. +description_cn: > + 火绒终端分组管理工具。通过 `action` 参数调用分组查询、创建、重命名和删除接口。 + 请在火绒服务配置中分别填写 Access Key ID、Secret Key 和 Base URL。 +category: custom +enabled: true +requires_confirmation: true +provider: huorong_api +inputSchema: + type: object + properties: + action: + type: string + description: | + 分组管理动作名,可选值: + - group_list + 用途: 获取全部分组列表 + 必填: 无 + 常用: 无 + 风险提示: 只读查询接口 + 是否任务型: 否 + - group_create + 用途: 创建分组 + 必填: `group_name` + 常用: `parent_group` + 风险提示: 写操作;会新增分组 + 是否任务型: 否 + - group_rename + 用途: 修改分组名称 + 必填: `group_id`、`group_name` + 常用: `group_id`、`group_name` + 风险提示: 写操作;会修改已有分组名称 + 是否任务型: 否 + - group_delete + 用途: 删除分组 + 必填: `group_id` + 常用: `group_id` + 风险提示: 高风险写操作;删除后分组及归属终端均受影响 + 是否任务型: 否 + enum: + - group_list + - group_create + - group_rename + - group_delete + group_id: + type: integer + description: 分组 ID + group_name: + type: string + description: 分组名称 + parent_group: + type: integer + description: 父分组 ID,默认为 0(根分组) + default: 0 + required: + - action +handler: + type: script + script_file: huorong.handler.py + function: group diff --git a/.flocks/flockshub/plugins/tools/device/huorong_edr_v1_0/huorong_task.yaml b/.flocks/flockshub/plugins/tools/device/huorong_edr_v1_0/huorong_task.yaml new file mode 100644 index 00000000..94144930 --- /dev/null +++ b/.flocks/flockshub/plugins/tools/device/huorong_edr_v1_0/huorong_task.yaml @@ -0,0 +1,36 @@ +name: huorong_task +description: > + Huorong EDR task management tool. Use the `action` parameter to create + virus scan tasks and query task results. +description_cn: > + 火绒任务管理工具。通过 `action` 参数调用查杀扫描任务创建和任务结果查询接口。 + 请在火绒服务配置中分别填写 Access Key ID、Secret Key 和 Base URL。 +category: custom +enabled: true +requires_confirmation: true +provider: huorong_api +inputSchema: + type: object + properties: + action: + type: string + description: | + 任务管理动作名,可选值: + - task_create + 用途: 创建查杀扫描任务 + 必填: 无(可传 offset 控制分页) + 常用: `offset` + 风险提示: 写操作;会向指定终端下发扫描任务 + 是否任务型: 是 + enum: + - task_create + offset: + type: integer + description: 分页偏移量,从 0 开始 + default: 0 + required: + - action +handler: + type: script + script_file: huorong.handler.py + function: task