ForcePilot/backend/package/yuxi/channels/adapters/feishu/security_audit.py
Kris 69b7139863 refactor(feishu): 清理冗余代码并补全飞书工具集实现
1.  移除多个文件中的空行、冗余导入
2.  修复文件末尾缺少换行符的问题
3.  新增并补全飞书多类工具API实现:
    - 多维表格:更新、删除记录,列出视图
    - 文档:更新、追加、删除块
    - 云文档:上传、下载文件
    - 群组:创建、添加成员、更新信息、创建公告
    - 目录:重构用户部门缓存逻辑
4.  优化消息发送、回复、转发等API的错误处理和逻辑
5.  新增消息列表查询、已读状态查询等功能
2026-05-13 16:07:59 +08:00

232 lines
8.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

from __future__ import annotations
from dataclasses import dataclass, field
from typing import Any
@dataclass
class SecurityWarning:
level: str
code: str
message: str
suggestion: str = ""
@dataclass
class SecurityAuditResult:
warnings: list[SecurityWarning] = field(default_factory=list)
@property
def has_critical(self) -> bool:
return any(w.level == "critical" for w in self.warnings)
@property
def has_warnings(self) -> bool:
return len(self.warnings) > 0
def audit_feishu_security(config: dict[str, Any]) -> SecurityAuditResult:
warnings: list[SecurityWarning] = []
group_policy = config.get("group_policy", config.get("groupPolicy", "allowlist"))
if group_policy == "allowall":
group_policy = "open"
warnings.append(
SecurityWarning(
level="info",
code="FEISHU_GROUP_POLICY_LEGACY_ALIAS",
message="groupPolicy 使用了已废弃的 'allowall' 值,已自动转换为 'open'",
suggestion="建议将配置从 'allowall' 更新为 'open'",
)
)
dm_policy = config.get("dm_policy", config.get("dmPolicy", "pairing"))
allowlist = config.get("allowlist", config.get("allowFrom", []))
default_account = config.get("defaultAccount", "")
if group_policy == "open":
warnings.append(
SecurityWarning(
level="warning",
code="FEISHU_GROUP_POLICY_OPEN",
message="群聊策略设置为 'open',所有群聊中的用户都可以与机器人交互",
suggestion="建议设置为 'allowlist' 并使用 allowFrom 限制可访问的群聊",
)
)
if group_policy == "disabled":
warnings.append(
SecurityWarning(
level="info",
code="FEISHU_GROUP_POLICY_DISABLED",
message="群聊功能已禁用",
suggestion="如需启用群聊功能,请修改 groupPolicy 为 'open''allowlist'",
)
)
if not allowlist:
if group_policy == "allowlist" or dm_policy == "allowlist":
warnings.append(
SecurityWarning(
level="critical",
code="FEISHU_EMPTY_ALLOWLIST",
message="allowFrom 白名单为空,但策略要求白名单验证",
suggestion="请在 allowFrom 中配置至少一个允许的用户或群组 ID",
)
)
if dm_policy == "open":
warnings.append(
SecurityWarning(
level="warning",
code="FEISHU_DM_POLICY_OPEN",
message="私聊策略设置为 'open',所有用户都可以向机器人发送私信",
suggestion="如需限制,可设置为 'pairing''allowlist'",
)
)
accounts = config.get("accounts", {})
if default_account and isinstance(accounts, dict) and default_account not in accounts:
warnings.append(
SecurityWarning(
level="warning",
code="FEISHU_DEFAULT_ACCOUNT_NOT_FOUND",
message=f"defaultAccount '{default_account}' 在 accounts 中未找到",
suggestion="请检查 defaultAccount 配置是否与 accounts 中的账户名称一致",
)
)
doc_owner_open_id = config.get("docOwnerOpenId", "")
if doc_owner_open_id:
warnings.append(
SecurityWarning(
level="info",
code="FEISHU_DOC_OWNER_SET",
message=f"文档所有者 open_id 已配置: {doc_owner_open_id[:8]}...",
suggestion="确认该 open_id 对应的用户拥有文档管理权限",
)
)
if not allowlist and group_policy == "allowlist":
warnings.append(
SecurityWarning(
level="critical",
code="FEISHU_ALLOWLIST_DEADLOCK",
message="allowlist 为空 + groupPolicy=allowlist所有群聊将被拒绝访问",
suggestion="请添加至少一个群聊到 allowlist 或修改 groupPolicy",
)
)
verify_token = config.get("verify_token", config.get("verifyToken", ""))
encrypt_key = config.get("encrypt_key", config.get("encryptKey", ""))
webhook_path = config.get("webhookPath", "")
if webhook_path and not verify_token and not encrypt_key:
warnings.append(
SecurityWarning(
level="critical",
code="FEISHU_WEBHOOK_NO_VERIFICATION",
message="Webhook 模式已启用但未配置 verifyToken 或 encryptKey存在安全隐患",
suggestion="请配置 verifyToken 或 encryptKey 以验证 Webhook 请求来源",
)
)
if not verify_token and not encrypt_key:
warnings.append(
SecurityWarning(
level="warning",
code="FEISHU_NO_VERIFICATION_CONFIG",
message="未配置 verifyToken 和 encryptKey事件回调请求将无法验证来源",
suggestion="在飞书开放平台获取并配置 verifyToken 或 encryptKey",
)
)
if app_secret := (config.get("app_secret") or config.get("appSecret") or ""):
if len(str(app_secret)) < 16:
warnings.append(
SecurityWarning(
level="warning",
code="FEISHU_APP_SECRET_TOO_SHORT",
message="appSecret 长度过短,可能存在安全风险",
suggestion="确认使用的 appSecret 是从飞书开发者后台直接获取的完整密钥",
)
)
if isinstance(accounts, dict) and len(accounts) > 5:
warnings.append(
SecurityWarning(
level="warning",
code="FEISHU_MANY_ACCOUNTS",
message=f"配置了 {len(accounts)} 个账户,请确认所有账户都在使用中",
suggestion="建议定期清理未使用的账户配置",
)
)
reactions_enabled = config.get("reactionsEnabled", True)
if not reactions_enabled:
warnings.append(
SecurityWarning(
level="info",
code="FEISHU_REACTIONS_DISABLED",
message="消息反应功能已禁用",
suggestion="启用反应功能可以改善用户体验,如需启用请设置 reactionsEnabled=True",
)
)
tools_enabled_count = sum(
1 for t in ("doc", "chat", "wiki", "drive", "perm") if config.get("tools", {}).get(t, True)
)
if tools_enabled_count >= 5:
warnings.append(
SecurityWarning(
level="warning",
code="FEISHU_ALL_TOOLS_ENABLED",
message=f"已启用 {tools_enabled_count} 个工具集成,请确保权限控制得当",
suggestion="建议检查工具集成权限映射,避免给用户过高的文档/通讯录操作权限",
)
)
encrypt_not_both = (verify_token and not encrypt_key) or (encrypt_key and not verify_token)
if encrypt_not_both:
warnings.append(
SecurityWarning(
level="info",
code="FEISHU_VERIFICATION_PARTIAL",
message="verifyToken 和 encryptKey 仅配置了其中一项",
suggestion="建议同时配置两项以获得完整的安全验证",
)
)
http_proxy = config.get("httpProxy", "")
if http_proxy:
warnings.append(
SecurityWarning(
level="info",
code="FEISHU_HTTP_PROXY_CONFIGURED",
message=f"已配置 HTTP 代理: {http_proxy[:50]}...",
suggestion="确认代理服务器地址和端口正确,并且网络连通性正常",
)
)
return SecurityAuditResult(warnings=warnings)
def collect_feishu_security_warnings(config: dict[str, Any]) -> list[SecurityWarning]:
result = audit_feishu_security(config)
return result.warnings
def collect_feishu_security_audit_findings(config: dict[str, Any]) -> list[dict]:
result = audit_feishu_security(config)
findings: list[dict] = []
for w in result.warnings:
findings.append(
{
"level": w.level,
"code": w.code,
"message": w.message,
"suggestion": w.suggestion,
}
)
return findings