from __future__ import annotations import enum from typing import Any ALLOW_ENTRY_PREFIXES = ("zoa:", "zalo_oa:") class DMPolicy(enum.StrEnum): OPEN = "open" PAIRING = "pairing" ALLOWLIST = "allowlist" DISABLED = "disabled" def normalize_allow_entry(entry: str) -> str: for prefix in ALLOW_ENTRY_PREFIXES: if entry.lower().startswith(prefix): return entry[len(prefix) :] return entry def resolve_dm_policy(config: dict[str, Any]) -> DMPolicy: policy_str = config.get("dm_policy", config.get("dmPolicy", "")) if policy_str == "open": return DMPolicy.OPEN if policy_str == "pairing": return DMPolicy.PAIRING if policy_str == "allowlist": return DMPolicy.ALLOWLIST if policy_str == "disabled": return DMPolicy.DISABLED return DMPolicy.OPEN def load_allowlist(config: dict[str, Any]) -> set[str]: raw = config.get("allowFrom", []) if not isinstance(raw, list): raw = [] if "*" in raw: return {"*"} return {normalize_allow_entry(str(entry)) for entry in raw} def check_dm_allowed(user_id: str, policy: DMPolicy, allowlist: set[str]) -> bool: if policy == DMPolicy.OPEN: return True if policy == DMPolicy.DISABLED: return False if policy == DMPolicy.ALLOWLIST: if "*" in allowlist: return True return user_id in allowlist if policy == DMPolicy.PAIRING: if "*" in allowlist: return True return user_id in allowlist return False def collect_security_warnings(config: dict[str, Any]) -> list[dict[str, Any]]: warnings: list[dict[str, Any]] = [] policy = resolve_dm_policy(config) allowlist = load_allowlist(config) webhook = config.get("webhook", {}) if policy == DMPolicy.OPEN: warnings.append( { "severity": "warning", "type": "dm_policy_open", "message": "DM policy is 'open' — any follower can send messages without restriction", "recommendation": "Consider switching to 'allowlist' or 'pairing' for production", } ) if policy == DMPolicy.ALLOWLIST and not allowlist: warnings.append( { "severity": "error", "type": "empty_allowlist", "message": "DM policy is 'allowlist' but allowFrom is empty — no users can message the OA", "recommendation": "Add user IDs to allowFrom or change policy to 'open'", } ) if policy == DMPolicy.PAIRING and not allowlist and "*" not in allowlist: warnings.append( { "severity": "info", "type": "pairing_no_preset", "message": "DM policy is 'pairing' with empty allowFrom — all pairing requests will be processed", "recommendation": "Add pre-approved user IDs to allowFrom for faster onboarding", } ) if policy == DMPolicy.ALLOWLIST and "*" in allowlist: warnings.append( { "severity": "info", "type": "allowlist_wildcard", "message": "allowFrom contains '*' wildcard — effectively same as 'open' policy", "recommendation": "Use '*' only in development or use 'open' policy explicitly", } ) if not webhook.get("url"): warnings.append( { "severity": "info", "type": "webhook_not_configured", "message": "Webhook URL not configured — messages will only be received via polling", "recommendation": "Configure a webhook URL for real-time message delivery", } ) if not webhook.get("mac_key"): warnings.append( { "severity": "warning", "type": "webhook_mac_key_missing", "message": "Webhook MAC key not configured — webhook signature verification is disabled", "recommendation": "Set ZALO_WEBHOOK_MAC_KEY or webhook.mac_key to enable signature verification", } ) return warnings