feat: 添加安全模块 + Dockerfile添加curl支持健康检查

主要更新:
- 新增 security/ 安全模块 (风险评估、威胁检测、蜜罐等)
- Dockerfile 添加 curl 以支持 Docker 健康检查
- 前端页面更新 (管理后台、用户端)
- 数据库迁移和 schema 更新
- 新增 kdocs 上传服务
- 添加安全相关测试用例

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Yu Yon
2026-01-08 17:48:33 +08:00
parent e3b0c35da6
commit 53c78e8e3c
76 changed files with 8563 additions and 4709 deletions

View File

@@ -0,0 +1,155 @@
from __future__ import annotations
import pytest
from flask import Flask, g, jsonify
from flask_login import LoginManager
import db_pool
from db.schema import ensure_schema
from security import init_security_middleware
@pytest.fixture()
def _test_db(tmp_path):
db_file = tmp_path / "security_middleware_test.db"
old_pool = getattr(db_pool, "_pool", None)
try:
if old_pool is not None:
try:
old_pool.close_all()
except Exception:
pass
db_pool._pool = None
db_pool.init_pool(str(db_file), pool_size=1)
with db_pool.get_db() as conn:
ensure_schema(conn)
yield db_file
finally:
try:
if getattr(db_pool, "_pool", None) is not None:
db_pool._pool.close_all()
except Exception:
pass
db_pool._pool = old_pool
def _make_app(monkeypatch, _test_db, *, security_enabled: bool = True, honeypot_enabled: bool = True) -> Flask:
import security.middleware as sm
import security.response_handler as rh
# 避免测试因风控延迟而变慢
monkeypatch.setattr(rh.time, "sleep", lambda _seconds: None)
# 每个测试用例保持 handler/honeypot 的懒加载状态
sm.handler = None
sm.honeypot = None
app = Flask(__name__)
app.config.update(
SECRET_KEY="test-secret",
TESTING=True,
SECURITY_ENABLED=bool(security_enabled),
HONEYPOT_ENABLED=bool(honeypot_enabled),
SECURITY_LOG_LEVEL="CRITICAL", # 降低测试日志噪音
)
login_manager = LoginManager()
login_manager.init_app(app)
@login_manager.user_loader
def _load_user(_user_id: str):
return None
init_security_middleware(app)
return app
def _client_get(app: Flask, path: str, *, ip: str = "1.2.3.4"):
return app.test_client().get(path, environ_overrides={"REMOTE_ADDR": ip})
def test_middleware_blocks_banned_ip(_test_db, monkeypatch):
app = _make_app(monkeypatch, _test_db)
@app.get("/api/ping")
def _ping():
return jsonify({"ok": True})
import security.middleware as sm
sm.blacklist.ban_ip("1.2.3.4", reason="test", duration_hours=1, permanent=False)
resp = _client_get(app, "/api/ping", ip="1.2.3.4")
assert resp.status_code == 503
assert resp.get_json() == {"error": "服务暂时繁忙,请稍后重试"}
def test_middleware_skips_static_requests(_test_db, monkeypatch):
app = _make_app(monkeypatch, _test_db)
@app.get("/static/test")
def _static_test():
return "ok"
import security.middleware as sm
sm.blacklist.ban_ip("1.2.3.4", reason="test", duration_hours=1, permanent=False)
resp = _client_get(app, "/static/test", ip="1.2.3.4")
assert resp.status_code == 200
assert resp.get_data(as_text=True) == "ok"
def test_middleware_honeypot_short_circuits_side_effects(_test_db, monkeypatch):
app = _make_app(monkeypatch, _test_db, honeypot_enabled=True)
called = {"count": 0}
@app.get("/api/side-effect")
def _side_effect():
called["count"] += 1
return jsonify({"real": True})
resp = _client_get(app, "/api/side-effect?q=${${a}}", ip="9.9.9.9")
assert resp.status_code == 200
payload = resp.get_json()
assert isinstance(payload, dict)
assert payload.get("success") is True
assert called["count"] == 0
def test_middleware_fails_open_on_internal_errors(_test_db, monkeypatch):
app = _make_app(monkeypatch, _test_db)
@app.get("/api/ok")
def _ok():
return jsonify({"ok": True, "risk_score": getattr(g, "risk_score", None)})
import security.middleware as sm
def boom(*_args, **_kwargs):
raise RuntimeError("boom")
monkeypatch.setattr(sm.blacklist, "is_ip_banned", boom)
monkeypatch.setattr(sm.detector, "scan_input", boom)
resp = _client_get(app, "/api/ok", ip="2.2.2.2")
assert resp.status_code == 200
assert resp.get_json()["ok"] is True
def test_middleware_sets_request_context_fields(_test_db, monkeypatch):
app = _make_app(monkeypatch, _test_db)
@app.get("/api/context")
def _context():
strategy = getattr(g, "response_strategy", None)
action = getattr(getattr(strategy, "action", None), "value", None)
return jsonify({"risk_score": getattr(g, "risk_score", None), "action": action})
resp = _client_get(app, "/api/context", ip="8.8.8.8")
assert resp.status_code == 200
assert resp.get_json() == {"risk_score": 0, "action": "allow"}