feat: 实现完整安全防护系统

Phase 1 - 威胁检测引擎:
- security/threat_detector.py: JNDI/SQL/XSS/路径遍历/命令注入检测
- security/constants.py: 威胁检测规则和评分常量
- 数据库表: threat_events, ip_risk_scores, user_risk_scores, ip_blacklist

Phase 2 - 风险评分与黑名单:
- security/risk_scorer.py: IP/用户风险评分引擎,支持分数衰减
- security/blacklist.py: 黑名单管理,自动封禁规则

Phase 3 - 响应策略:
- security/honeypot.py: 蜜罐响应生成器
- security/response_handler.py: 渐进式响应策略

Phase 4 - 集成:
- security/middleware.py: Flask安全中间件
- routes/admin_api/security.py: 管理后台安全仪表板API
- 36个测试用例全部通过

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2025-12-27 01:28:38 +08:00
parent e3b0c35da6
commit 46253337eb
24 changed files with 3219 additions and 4 deletions

View File

@@ -0,0 +1,334 @@
#!/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/<ip>", 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/<int:user_id>", 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})