#!/usr/bin/env python3 # -*- coding: utf-8 -*- from __future__ import annotations from typing import Any from flask import Blueprint, jsonify, request import db_pool from db import security as security_db from routes.decorators import admin_required from security import BlacklistManager, RiskScorer security_bp = Blueprint("admin_security", __name__) blacklist = BlacklistManager() scorer = RiskScorer(blacklist_manager=blacklist) def _truncate(value: Any, max_len: int = 200) -> str: text = str(value or "") if max_len <= 0: return "" if len(text) <= max_len: return text return text[: max(0, max_len - 3)] + "..." def _parse_int_arg(name: str, default: int, *, min_value: int | None = None, max_value: int | None = None) -> int: raw = request.args.get(name, None) if raw is None or str(raw).strip() == "": value = int(default) else: try: value = int(str(raw).strip()) except Exception: value = int(default) if min_value is not None: value = max(int(min_value), value) if max_value is not None: value = min(int(max_value), value) return value def _parse_json() -> dict: if request.is_json: data = request.get_json(silent=True) or {} return data if isinstance(data, dict) else {} # 兼容 form-data try: return dict(request.form or {}) except Exception: return {} def _parse_bool(value: Any) -> bool: if isinstance(value, bool): return value if isinstance(value, int): return value != 0 text = str(value or "").strip().lower() return text in {"1", "true", "yes", "y", "on"} def _sanitize_threat_event(event: dict) -> dict: return { "id": event.get("id"), "threat_type": event.get("threat_type") or "unknown", "score": int(event.get("score") or 0), "ip": _truncate(event.get("ip"), 64), "user_id": event.get("user_id"), "request_method": _truncate(event.get("request_method"), 16), "request_path": _truncate(event.get("request_path"), 256), "field_name": _truncate(event.get("field_name"), 80), "rule": _truncate(event.get("rule"), 120), "matched": _truncate(event.get("matched"), 120), "value_preview": _truncate(event.get("value_preview"), 200), "created_at": event.get("created_at"), } def _sanitize_ban_entry(entry: dict, *, kind: str) -> dict: if kind == "ip": return { "ip": _truncate(entry.get("ip"), 64), "reason": _truncate(entry.get("reason"), 200), "added_at": entry.get("added_at"), "expires_at": entry.get("expires_at"), "is_active": int(entry.get("is_active") or 0), } if kind == "user": return { "user_id": entry.get("user_id"), "reason": _truncate(entry.get("reason"), 200), "added_at": entry.get("added_at"), "expires_at": entry.get("expires_at"), "is_active": int(entry.get("is_active") or 0), } return {} @security_bp.route("/api/admin/security/dashboard", methods=["GET"]) @admin_required def get_security_dashboard(): """ 获取安全仪表板数据 返回: - 最近24小时威胁事件数 - 当前封禁IP数 - 当前封禁用户数 - 最近10条威胁事件 """ try: threat_24h = security_db.get_threat_events_count(hours=24) except Exception: threat_24h = 0 try: banned_ips = blacklist.get_banned_ips() except Exception: banned_ips = [] try: banned_users = blacklist.get_banned_users() except Exception: banned_users = [] try: recent = security_db.get_threat_events_list(page=1, per_page=10, filters={}).get("items", []) recent_items = [_sanitize_threat_event(e) for e in recent if isinstance(e, dict)] except Exception: recent_items = [] return jsonify( { "threat_events_24h": int(threat_24h or 0), "banned_ip_count": len(banned_ips), "banned_user_count": len(banned_users), "recent_threat_events": recent_items, } ) @security_bp.route("/api/admin/security/threats", methods=["GET"]) @admin_required def get_threat_events(): """ 获取威胁事件列表(分页) 参数: page, per_page, severity, event_type """ page = _parse_int_arg("page", 1, min_value=1, max_value=100000) per_page = _parse_int_arg("per_page", 20, min_value=1, max_value=200) severity = (request.args.get("severity") or "").strip() event_type = (request.args.get("event_type") or "").strip() filters: dict[str, Any] = {} if severity: filters["severity"] = severity if event_type: filters["event_type"] = event_type data = security_db.get_threat_events_list(page, per_page, filters) items = data.get("items") or [] data["items"] = [_sanitize_threat_event(e) for e in items if isinstance(e, dict)] return jsonify(data) @security_bp.route("/api/admin/security/banned-ips", methods=["GET"]) @admin_required def get_banned_ips(): """获取封禁IP列表""" items = blacklist.get_banned_ips() return jsonify({"count": len(items), "items": [_sanitize_ban_entry(x, kind="ip") for x in items]}) @security_bp.route("/api/admin/security/banned-users", methods=["GET"]) @admin_required def get_banned_users(): """获取封禁用户列表""" items = blacklist.get_banned_users() return jsonify({"count": len(items), "items": [_sanitize_ban_entry(x, kind="user") for x in items]}) @security_bp.route("/api/admin/security/ban-ip", methods=["POST"]) @admin_required def ban_ip(): """ 手动封禁IP 参数: ip, reason, duration_hours(可选), permanent(可选) """ data = _parse_json() ip = str(data.get("ip") or "").strip() reason = str(data.get("reason") or "").strip() duration_hours_raw = data.get("duration_hours", 24) permanent = _parse_bool(data.get("permanent", False)) if not ip: return jsonify({"error": "ip不能为空"}), 400 if not reason: return jsonify({"error": "reason不能为空"}), 400 try: duration_hours = max(1, int(duration_hours_raw)) except Exception: duration_hours = 24 ok = blacklist.ban_ip(ip, reason, duration_hours=duration_hours, permanent=permanent) if not ok: return jsonify({"error": "封禁失败"}), 400 return jsonify({"success": True}) @security_bp.route("/api/admin/security/unban-ip", methods=["POST"]) @admin_required def unban_ip(): """解除IP封禁""" data = _parse_json() ip = str(data.get("ip") or "").strip() if not ip: return jsonify({"error": "ip不能为空"}), 400 ok = blacklist.unban_ip(ip) if not ok: return jsonify({"error": "未找到封禁记录"}), 404 return jsonify({"success": True}) @security_bp.route("/api/admin/security/ban-user", methods=["POST"]) @admin_required def ban_user(): """手动封禁用户""" data = _parse_json() user_id_raw = data.get("user_id") reason = str(data.get("reason") or "").strip() duration_hours_raw = data.get("duration_hours", 24) permanent = _parse_bool(data.get("permanent", False)) try: user_id = int(user_id_raw) except Exception: user_id = None if user_id is None: return jsonify({"error": "user_id不能为空"}), 400 if not reason: return jsonify({"error": "reason不能为空"}), 400 try: duration_hours = max(1, int(duration_hours_raw)) except Exception: duration_hours = 24 ok = blacklist._ban_user_internal(user_id, reason=reason, duration_hours=duration_hours, permanent=permanent) if not ok: return jsonify({"error": "封禁失败"}), 400 return jsonify({"success": True}) @security_bp.route("/api/admin/security/unban-user", methods=["POST"]) @admin_required def unban_user(): """解除用户封禁""" data = _parse_json() user_id_raw = data.get("user_id") try: user_id = int(user_id_raw) except Exception: user_id = None if user_id is None: return jsonify({"error": "user_id不能为空"}), 400 ok = blacklist.unban_user(user_id) if not ok: return jsonify({"error": "未找到封禁记录"}), 404 return jsonify({"success": True}) @security_bp.route("/api/admin/security/ip-risk/", methods=["GET"]) @admin_required def get_ip_risk(ip): """获取指定IP的风险评分和历史事件""" ip_text = str(ip or "").strip() if not ip_text: return jsonify({"error": "ip不能为空"}), 400 history = security_db.get_ip_threat_history(ip_text) return jsonify( { "ip": _truncate(ip_text, 64), "risk_score": int(scorer.get_ip_score(ip_text) or 0), "is_banned": bool(blacklist.is_ip_banned(ip_text)), "threat_history": [_sanitize_threat_event(e) for e in history if isinstance(e, dict)], } ) @security_bp.route("/api/admin/security/user-risk/", methods=["GET"]) @admin_required def get_user_risk(user_id): """获取指定用户的风险评分和历史事件""" history = security_db.get_user_threat_history(user_id) return jsonify( { "user_id": int(user_id), "risk_score": int(scorer.get_user_score(user_id) or 0), "is_banned": bool(blacklist.is_user_banned(user_id)), "threat_history": [_sanitize_threat_event(e) for e in history if isinstance(e, dict)], } ) @security_bp.route("/api/admin/security/cleanup", methods=["POST"]) @admin_required def cleanup_expired(): """清理过期的封禁记录和衰减风险分""" try: blacklist.cleanup_expired() except Exception: pass try: scorer.decay_scores() except Exception: pass # 可选:返回当前连接池统计信息,便于排查后台运行状态 pool_stats = None try: pool_stats = db_pool.get_pool_stats() except Exception: pool_stats = None return jsonify({"success": True, "pool_stats": pool_stats})