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>
642 lines
24 KiB
Python
642 lines
24 KiB
Python
#!/usr/bin/env python3
|
||
# -*- coding: utf-8 -*-
|
||
from __future__ import annotations
|
||
|
||
import sqlite3
|
||
|
||
from db.utils import get_cst_now_str
|
||
|
||
|
||
def get_current_version(conn) -> int:
|
||
cursor = conn.cursor()
|
||
cursor.execute("SELECT version FROM db_version WHERE id = 1")
|
||
row = cursor.fetchone()
|
||
if not row:
|
||
return 0
|
||
try:
|
||
return int(row["version"])
|
||
except Exception:
|
||
try:
|
||
return int(row[0])
|
||
except Exception:
|
||
return 0
|
||
|
||
|
||
def set_current_version(conn, version: int) -> None:
|
||
cursor = conn.cursor()
|
||
cursor.execute("UPDATE db_version SET version = ?, updated_at = ? WHERE id = 1", (int(version), get_cst_now_str()))
|
||
conn.commit()
|
||
|
||
|
||
def migrate_database(conn, target_version: int) -> None:
|
||
"""数据库迁移:按版本增量升级(向前兼容)。"""
|
||
cursor = conn.cursor()
|
||
cursor.execute("INSERT OR IGNORE INTO db_version (id, version, updated_at) VALUES (1, 0, ?)", (get_cst_now_str(),))
|
||
conn.commit()
|
||
|
||
current_version = get_current_version(conn)
|
||
|
||
if current_version < 1:
|
||
_migrate_to_v1(conn)
|
||
current_version = 1
|
||
if current_version < 2:
|
||
_migrate_to_v2(conn)
|
||
current_version = 2
|
||
if current_version < 3:
|
||
_migrate_to_v3(conn)
|
||
current_version = 3
|
||
if current_version < 4:
|
||
_migrate_to_v4(conn)
|
||
current_version = 4
|
||
if current_version < 5:
|
||
_migrate_to_v5(conn)
|
||
current_version = 5
|
||
if current_version < 6:
|
||
_migrate_to_v6(conn)
|
||
current_version = 6
|
||
if current_version < 7:
|
||
_migrate_to_v7(conn)
|
||
current_version = 7
|
||
if current_version < 8:
|
||
_migrate_to_v8(conn)
|
||
current_version = 8
|
||
if current_version < 9:
|
||
_migrate_to_v9(conn)
|
||
current_version = 9
|
||
if current_version < 10:
|
||
_migrate_to_v10(conn)
|
||
current_version = 10
|
||
if current_version < 11:
|
||
_migrate_to_v11(conn)
|
||
current_version = 11
|
||
if current_version < 12:
|
||
_migrate_to_v12(conn)
|
||
current_version = 12
|
||
if current_version < 13:
|
||
_migrate_to_v13(conn)
|
||
current_version = 13
|
||
if current_version < 14:
|
||
_migrate_to_v14(conn)
|
||
current_version = 14
|
||
|
||
if current_version != int(target_version):
|
||
set_current_version(conn, int(target_version))
|
||
|
||
|
||
def _migrate_to_v1(conn):
|
||
"""迁移到版本1 - 添加缺失字段"""
|
||
cursor = conn.cursor()
|
||
|
||
cursor.execute("PRAGMA table_info(system_config)")
|
||
columns = [col[1] for col in cursor.fetchall()]
|
||
|
||
if "schedule_weekdays" not in columns:
|
||
cursor.execute('ALTER TABLE system_config ADD COLUMN schedule_weekdays TEXT DEFAULT "1,2,3,4,5,6,7"')
|
||
print(" ✓ 添加 schedule_weekdays 字段")
|
||
|
||
if "max_screenshot_concurrent" not in columns:
|
||
cursor.execute("ALTER TABLE system_config ADD COLUMN max_screenshot_concurrent INTEGER DEFAULT 3")
|
||
print(" ✓ 添加 max_screenshot_concurrent 字段")
|
||
if "max_concurrent_per_account" not in columns:
|
||
cursor.execute("ALTER TABLE system_config ADD COLUMN max_concurrent_per_account INTEGER DEFAULT 1")
|
||
print(" ✓ 添加 max_concurrent_per_account 字段")
|
||
if "auto_approve_enabled" not in columns:
|
||
cursor.execute("ALTER TABLE system_config ADD COLUMN auto_approve_enabled INTEGER DEFAULT 0")
|
||
print(" ✓ 添加 auto_approve_enabled 字段")
|
||
if "auto_approve_hourly_limit" not in columns:
|
||
cursor.execute("ALTER TABLE system_config ADD COLUMN auto_approve_hourly_limit INTEGER DEFAULT 10")
|
||
print(" ✓ 添加 auto_approve_hourly_limit 字段")
|
||
if "auto_approve_vip_days" not in columns:
|
||
cursor.execute("ALTER TABLE system_config ADD COLUMN auto_approve_vip_days INTEGER DEFAULT 7")
|
||
print(" ✓ 添加 auto_approve_vip_days 字段")
|
||
|
||
cursor.execute("PRAGMA table_info(task_logs)")
|
||
columns = [col[1] for col in cursor.fetchall()]
|
||
if "duration" not in columns:
|
||
cursor.execute("ALTER TABLE task_logs ADD COLUMN duration INTEGER")
|
||
print(" ✓ 添加 duration 字段到 task_logs")
|
||
|
||
conn.commit()
|
||
|
||
|
||
def _migrate_to_v2(conn):
|
||
"""迁移到版本2 - 添加代理配置字段"""
|
||
cursor = conn.cursor()
|
||
|
||
cursor.execute("PRAGMA table_info(system_config)")
|
||
columns = [col[1] for col in cursor.fetchall()]
|
||
|
||
if "proxy_enabled" not in columns:
|
||
cursor.execute("ALTER TABLE system_config ADD COLUMN proxy_enabled INTEGER DEFAULT 0")
|
||
print(" ✓ 添加 proxy_enabled 字段")
|
||
|
||
if "proxy_api_url" not in columns:
|
||
cursor.execute('ALTER TABLE system_config ADD COLUMN proxy_api_url TEXT DEFAULT ""')
|
||
print(" ✓ 添加 proxy_api_url 字段")
|
||
|
||
if "proxy_expire_minutes" not in columns:
|
||
cursor.execute("ALTER TABLE system_config ADD COLUMN proxy_expire_minutes INTEGER DEFAULT 3")
|
||
print(" ✓ 添加 proxy_expire_minutes 字段")
|
||
|
||
if "enable_screenshot" not in columns:
|
||
cursor.execute("ALTER TABLE system_config ADD COLUMN enable_screenshot INTEGER DEFAULT 1")
|
||
print(" ✓ 添加 enable_screenshot 字段")
|
||
|
||
conn.commit()
|
||
|
||
|
||
def _migrate_to_v3(conn):
|
||
"""迁移到版本3 - 添加账号状态和登录失败计数字段"""
|
||
cursor = conn.cursor()
|
||
|
||
cursor.execute("PRAGMA table_info(accounts)")
|
||
columns = [col[1] for col in cursor.fetchall()]
|
||
|
||
if "status" not in columns:
|
||
cursor.execute('ALTER TABLE accounts ADD COLUMN status TEXT DEFAULT "active"')
|
||
print(" ✓ 添加 accounts.status 字段 (账号状态)")
|
||
|
||
if "login_fail_count" not in columns:
|
||
cursor.execute("ALTER TABLE accounts ADD COLUMN login_fail_count INTEGER DEFAULT 0")
|
||
print(" ✓ 添加 accounts.login_fail_count 字段 (登录失败计数)")
|
||
|
||
if "last_login_error" not in columns:
|
||
cursor.execute("ALTER TABLE accounts ADD COLUMN last_login_error TEXT")
|
||
print(" ✓ 添加 accounts.last_login_error 字段 (最后登录错误)")
|
||
|
||
conn.commit()
|
||
|
||
|
||
def _migrate_to_v4(conn):
|
||
"""迁移到版本4 - 添加任务来源字段"""
|
||
cursor = conn.cursor()
|
||
|
||
cursor.execute("PRAGMA table_info(task_logs)")
|
||
columns = [col[1] for col in cursor.fetchall()]
|
||
|
||
if "source" not in columns:
|
||
cursor.execute('ALTER TABLE task_logs ADD COLUMN source TEXT DEFAULT "manual"')
|
||
print(" ✓ 添加 task_logs.source 字段 (任务来源: manual/scheduled/immediate)")
|
||
|
||
conn.commit()
|
||
|
||
|
||
def _migrate_to_v5(conn):
|
||
"""迁移到版本5 - 添加用户定时任务表"""
|
||
cursor = conn.cursor()
|
||
|
||
cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='user_schedules'")
|
||
if not cursor.fetchone():
|
||
cursor.execute(
|
||
"""
|
||
CREATE TABLE IF NOT EXISTS user_schedules (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
user_id INTEGER NOT NULL,
|
||
name TEXT DEFAULT '我的定时任务',
|
||
enabled INTEGER DEFAULT 0,
|
||
schedule_time TEXT NOT NULL DEFAULT '08:00',
|
||
weekdays TEXT NOT NULL DEFAULT '1,2,3,4,5',
|
||
browse_type TEXT NOT NULL DEFAULT '应读',
|
||
enable_screenshot INTEGER DEFAULT 1,
|
||
account_ids TEXT,
|
||
last_run_at TIMESTAMP,
|
||
next_run_at TIMESTAMP,
|
||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
|
||
)
|
||
"""
|
||
)
|
||
print(" ✓ 创建 user_schedules 表 (用户定时任务)")
|
||
|
||
cursor.execute(
|
||
"""
|
||
CREATE TABLE IF NOT EXISTS schedule_execution_logs (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
schedule_id INTEGER NOT NULL,
|
||
user_id INTEGER NOT NULL,
|
||
schedule_name TEXT,
|
||
execute_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||
total_accounts INTEGER DEFAULT 0,
|
||
success_accounts INTEGER DEFAULT 0,
|
||
failed_accounts INTEGER DEFAULT 0,
|
||
total_items INTEGER DEFAULT 0,
|
||
total_attachments INTEGER DEFAULT 0,
|
||
total_screenshots INTEGER DEFAULT 0,
|
||
duration_seconds INTEGER DEFAULT 0,
|
||
status TEXT DEFAULT 'running',
|
||
error_message TEXT,
|
||
FOREIGN KEY (schedule_id) REFERENCES user_schedules (id) ON DELETE CASCADE,
|
||
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
|
||
)
|
||
"""
|
||
)
|
||
print(" ✓ 创建 schedule_execution_logs 表 (定时任务执行日志)")
|
||
|
||
cursor.execute("CREATE INDEX IF NOT EXISTS idx_user_schedules_user_id ON user_schedules(user_id)")
|
||
cursor.execute("CREATE INDEX IF NOT EXISTS idx_user_schedules_enabled ON user_schedules(enabled)")
|
||
cursor.execute("CREATE INDEX IF NOT EXISTS idx_user_schedules_next_run ON user_schedules(next_run_at)")
|
||
print(" ✓ 创建 user_schedules 表索引")
|
||
|
||
conn.commit()
|
||
|
||
|
||
def _migrate_to_v6(conn):
|
||
"""迁移到版本6 - 添加公告功能相关表"""
|
||
cursor = conn.cursor()
|
||
|
||
cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='announcements'")
|
||
if not cursor.fetchone():
|
||
cursor.execute(
|
||
"""
|
||
CREATE TABLE IF NOT EXISTS announcements (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
title TEXT NOT NULL,
|
||
content TEXT NOT NULL,
|
||
is_active INTEGER DEFAULT 1,
|
||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||
)
|
||
"""
|
||
)
|
||
print(" ✓ 创建 announcements 表 (公告)")
|
||
cursor.execute("CREATE INDEX IF NOT EXISTS idx_announcements_active ON announcements(is_active)")
|
||
cursor.execute("CREATE INDEX IF NOT EXISTS idx_announcements_created_at ON announcements(created_at)")
|
||
print(" ✓ 创建 announcements 表索引")
|
||
|
||
cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='announcement_dismissals'")
|
||
if not cursor.fetchone():
|
||
cursor.execute(
|
||
"""
|
||
CREATE TABLE IF NOT EXISTS announcement_dismissals (
|
||
user_id INTEGER NOT NULL,
|
||
announcement_id INTEGER NOT NULL,
|
||
dismissed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||
PRIMARY KEY (user_id, announcement_id),
|
||
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE,
|
||
FOREIGN KEY (announcement_id) REFERENCES announcements (id) ON DELETE CASCADE
|
||
)
|
||
"""
|
||
)
|
||
print(" ✓ 创建 announcement_dismissals 表 (公告永久关闭记录)")
|
||
cursor.execute("CREATE INDEX IF NOT EXISTS idx_announcement_dismissals_user ON announcement_dismissals(user_id)")
|
||
print(" ✓ 创建 announcement_dismissals 表索引")
|
||
|
||
conn.commit()
|
||
|
||
|
||
def _migrate_to_v7(conn):
|
||
"""迁移到版本7 - 统一存储北京时间(将历史UTC时间字段整体+8小时)"""
|
||
cursor = conn.cursor()
|
||
|
||
def table_exists(table_name: str) -> bool:
|
||
cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name=?", (table_name,))
|
||
return cursor.fetchone() is not None
|
||
|
||
def column_exists(table_name: str, column_name: str) -> bool:
|
||
cursor.execute(f"PRAGMA table_info({table_name})")
|
||
return any(row[1] == column_name for row in cursor.fetchall())
|
||
|
||
def shift_utc_to_cst(table_name: str, column_name: str) -> None:
|
||
if not table_exists(table_name):
|
||
return
|
||
if not column_exists(table_name, column_name):
|
||
return
|
||
cursor.execute(
|
||
f"""
|
||
UPDATE {table_name}
|
||
SET {column_name} = datetime({column_name}, '+8 hours')
|
||
WHERE {column_name} IS NOT NULL AND {column_name} != ''
|
||
"""
|
||
)
|
||
|
||
for table, col in [
|
||
("users", "created_at"),
|
||
("users", "approved_at"),
|
||
("admins", "created_at"),
|
||
("accounts", "created_at"),
|
||
("password_reset_requests", "created_at"),
|
||
("password_reset_requests", "processed_at"),
|
||
]:
|
||
shift_utc_to_cst(table, col)
|
||
|
||
for table, col in [
|
||
("smtp_configs", "created_at"),
|
||
("smtp_configs", "updated_at"),
|
||
("smtp_configs", "last_success_at"),
|
||
("email_settings", "updated_at"),
|
||
("email_tokens", "created_at"),
|
||
("email_logs", "created_at"),
|
||
("email_stats", "last_updated"),
|
||
]:
|
||
shift_utc_to_cst(table, col)
|
||
|
||
for table, col in [
|
||
("task_checkpoints", "created_at"),
|
||
("task_checkpoints", "updated_at"),
|
||
("task_checkpoints", "completed_at"),
|
||
]:
|
||
shift_utc_to_cst(table, col)
|
||
|
||
conn.commit()
|
||
print(" ✓ 时区迁移:历史UTC时间已转换为北京时间")
|
||
|
||
|
||
def _migrate_to_v8(conn):
|
||
"""迁移到版本8 - 用户定时 next_run_at 随机延迟落库(O-08)"""
|
||
cursor = conn.cursor()
|
||
|
||
# 1) 增量字段:random_delay(旧库可能不存在)
|
||
cursor.execute("PRAGMA table_info(user_schedules)")
|
||
columns = [col[1] for col in cursor.fetchall()]
|
||
if "random_delay" not in columns:
|
||
cursor.execute("ALTER TABLE user_schedules ADD COLUMN random_delay INTEGER DEFAULT 0")
|
||
print(" ✓ 添加 user_schedules.random_delay 字段")
|
||
|
||
if "next_run_at" not in columns:
|
||
cursor.execute("ALTER TABLE user_schedules ADD COLUMN next_run_at TIMESTAMP")
|
||
print(" ✓ 添加 user_schedules.next_run_at 字段")
|
||
|
||
cursor.execute("CREATE INDEX IF NOT EXISTS idx_user_schedules_next_run ON user_schedules(next_run_at)")
|
||
conn.commit()
|
||
|
||
# 2) 为历史 enabled schedule 补算 next_run_at(保证索引驱动可用)
|
||
try:
|
||
from services.schedule_utils import compute_next_run_at, format_cst
|
||
from services.time_utils import get_beijing_now
|
||
|
||
now_dt = get_beijing_now()
|
||
now_str = format_cst(now_dt)
|
||
|
||
cursor.execute(
|
||
"""
|
||
SELECT id, schedule_time, weekdays, random_delay, last_run_at, next_run_at
|
||
FROM user_schedules
|
||
WHERE enabled = 1
|
||
"""
|
||
)
|
||
rows = cursor.fetchall() or []
|
||
|
||
fixed = 0
|
||
for row in rows:
|
||
try:
|
||
schedule_id = row["id"] if isinstance(row, sqlite3.Row) else row[0]
|
||
schedule_time = row["schedule_time"] if isinstance(row, sqlite3.Row) else row[1]
|
||
weekdays = row["weekdays"] if isinstance(row, sqlite3.Row) else row[2]
|
||
random_delay = row["random_delay"] if isinstance(row, sqlite3.Row) else row[3]
|
||
last_run_at = row["last_run_at"] if isinstance(row, sqlite3.Row) else row[4]
|
||
next_run_at = row["next_run_at"] if isinstance(row, sqlite3.Row) else row[5]
|
||
except Exception:
|
||
continue
|
||
|
||
next_run_text = str(next_run_at or "").strip()
|
||
# 若 next_run_at 为空/非法/已过期,则重算
|
||
if (not next_run_text) or (next_run_text <= now_str):
|
||
next_dt = compute_next_run_at(
|
||
now=now_dt,
|
||
schedule_time=str(schedule_time or "08:00"),
|
||
weekdays=str(weekdays or "1,2,3,4,5"),
|
||
random_delay=int(random_delay or 0),
|
||
last_run_at=str(last_run_at or "") if last_run_at else None,
|
||
)
|
||
next_run_text = format_cst(next_dt)
|
||
cursor.execute(
|
||
"UPDATE user_schedules SET next_run_at = ?, updated_at = ? WHERE id = ?",
|
||
(next_run_text, get_cst_now_str(), int(schedule_id)),
|
||
)
|
||
fixed += 1
|
||
|
||
conn.commit()
|
||
if fixed:
|
||
print(f" ✓ 已为 {fixed} 条启用定时任务补算 next_run_at")
|
||
except Exception as e:
|
||
# 迁移过程中不阻断主流程;上线后由 worker 兜底补算
|
||
print(f" ⚠ v8 迁移补算 next_run_at 失败: {e}")
|
||
|
||
|
||
def _migrate_to_v9(conn):
|
||
"""迁移到版本9 - 邮件设置字段迁移(清理 email_service scattered ALTER TABLE)"""
|
||
cursor = conn.cursor()
|
||
|
||
cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='email_settings'")
|
||
if not cursor.fetchone():
|
||
# 邮件表由 email_service.init_email_tables 创建;此处仅做增量字段迁移
|
||
return
|
||
|
||
cursor.execute("PRAGMA table_info(email_settings)")
|
||
columns = [col[1] for col in cursor.fetchall()]
|
||
|
||
changed = False
|
||
if "register_verify_enabled" not in columns:
|
||
cursor.execute("ALTER TABLE email_settings ADD COLUMN register_verify_enabled INTEGER DEFAULT 0")
|
||
print(" ✓ 添加 email_settings.register_verify_enabled 字段")
|
||
changed = True
|
||
if "base_url" not in columns:
|
||
cursor.execute("ALTER TABLE email_settings ADD COLUMN base_url TEXT DEFAULT ''")
|
||
print(" ✓ 添加 email_settings.base_url 字段")
|
||
changed = True
|
||
if "task_notify_enabled" not in columns:
|
||
cursor.execute("ALTER TABLE email_settings ADD COLUMN task_notify_enabled INTEGER DEFAULT 0")
|
||
print(" ✓ 添加 email_settings.task_notify_enabled 字段")
|
||
changed = True
|
||
|
||
if changed:
|
||
conn.commit()
|
||
|
||
|
||
def _migrate_to_v10(conn):
|
||
"""迁移到版本10 - users 邮箱字段迁移(避免运行时 ALTER TABLE)"""
|
||
cursor = conn.cursor()
|
||
cursor.execute("PRAGMA table_info(users)")
|
||
columns = [col[1] for col in cursor.fetchall()]
|
||
|
||
changed = False
|
||
if "email_verified" not in columns:
|
||
cursor.execute("ALTER TABLE users ADD COLUMN email_verified INTEGER DEFAULT 0")
|
||
print(" ✓ 添加 users.email_verified 字段")
|
||
changed = True
|
||
if "email_notify_enabled" not in columns:
|
||
cursor.execute("ALTER TABLE users ADD COLUMN email_notify_enabled INTEGER DEFAULT 1")
|
||
print(" ✓ 添加 users.email_notify_enabled 字段")
|
||
changed = True
|
||
|
||
if changed:
|
||
conn.commit()
|
||
|
||
|
||
def _migrate_to_v11(conn):
|
||
"""迁移到版本11 - 取消注册待审核:历史 pending 用户直接置为 approved"""
|
||
cursor = conn.cursor()
|
||
now_str = get_cst_now_str()
|
||
|
||
try:
|
||
cursor.execute(
|
||
"""
|
||
UPDATE users
|
||
SET status = 'approved',
|
||
approved_at = COALESCE(NULLIF(approved_at, ''), ?)
|
||
WHERE status = 'pending'
|
||
""",
|
||
(now_str,),
|
||
)
|
||
updated = cursor.rowcount
|
||
conn.commit()
|
||
|
||
if updated:
|
||
print(f" ✓ 已将 {updated} 个 pending 用户迁移为 approved")
|
||
except sqlite3.OperationalError as e:
|
||
print(f" ⚠️ v11 迁移跳过: {e}")
|
||
|
||
|
||
def _migrate_to_v12(conn):
|
||
"""迁移到版本12 - 登录设备/IP记录表"""
|
||
cursor = conn.cursor()
|
||
|
||
cursor.execute(
|
||
"""
|
||
CREATE TABLE IF NOT EXISTS login_fingerprints (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
user_id INTEGER NOT NULL,
|
||
user_agent TEXT NOT NULL,
|
||
first_seen TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||
last_seen TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||
last_ip TEXT DEFAULT '',
|
||
UNIQUE (user_id, user_agent),
|
||
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
|
||
)
|
||
"""
|
||
)
|
||
|
||
cursor.execute(
|
||
"""
|
||
CREATE TABLE IF NOT EXISTS login_ips (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
user_id INTEGER NOT NULL,
|
||
ip TEXT NOT NULL,
|
||
first_seen TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||
last_seen TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||
UNIQUE (user_id, ip),
|
||
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
|
||
)
|
||
"""
|
||
)
|
||
|
||
cursor.execute("CREATE INDEX IF NOT EXISTS idx_login_fingerprints_user ON login_fingerprints(user_id)")
|
||
cursor.execute("CREATE INDEX IF NOT EXISTS idx_login_ips_user ON login_ips(user_id)")
|
||
|
||
conn.commit()
|
||
|
||
|
||
def _migrate_to_v13(conn):
|
||
"""迁移到版本13 - 安全防护:威胁检测相关表"""
|
||
cursor = conn.cursor()
|
||
|
||
cursor.execute(
|
||
"""
|
||
CREATE TABLE IF NOT EXISTS threat_events (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
threat_type TEXT NOT NULL,
|
||
score INTEGER NOT NULL DEFAULT 0,
|
||
rule TEXT,
|
||
field_name TEXT,
|
||
matched TEXT,
|
||
value_preview TEXT,
|
||
ip TEXT,
|
||
user_id INTEGER,
|
||
request_method TEXT,
|
||
request_path TEXT,
|
||
user_agent TEXT,
|
||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
|
||
)
|
||
"""
|
||
)
|
||
cursor.execute("CREATE INDEX IF NOT EXISTS idx_threat_events_created_at ON threat_events(created_at)")
|
||
cursor.execute("CREATE INDEX IF NOT EXISTS idx_threat_events_ip ON threat_events(ip)")
|
||
cursor.execute("CREATE INDEX IF NOT EXISTS idx_threat_events_user_id ON threat_events(user_id)")
|
||
cursor.execute("CREATE INDEX IF NOT EXISTS idx_threat_events_type ON threat_events(threat_type)")
|
||
|
||
cursor.execute(
|
||
"""
|
||
CREATE TABLE IF NOT EXISTS ip_risk_scores (
|
||
ip TEXT PRIMARY KEY,
|
||
risk_score INTEGER NOT NULL DEFAULT 0,
|
||
last_seen TIMESTAMP,
|
||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||
)
|
||
"""
|
||
)
|
||
cursor.execute("CREATE INDEX IF NOT EXISTS idx_ip_risk_scores_score ON ip_risk_scores(risk_score)")
|
||
cursor.execute("CREATE INDEX IF NOT EXISTS idx_ip_risk_scores_updated_at ON ip_risk_scores(updated_at)")
|
||
|
||
cursor.execute(
|
||
"""
|
||
CREATE TABLE IF NOT EXISTS user_risk_scores (
|
||
user_id INTEGER PRIMARY KEY,
|
||
risk_score INTEGER NOT NULL DEFAULT 0,
|
||
last_seen TIMESTAMP,
|
||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
|
||
)
|
||
"""
|
||
)
|
||
cursor.execute("CREATE INDEX IF NOT EXISTS idx_user_risk_scores_score ON user_risk_scores(risk_score)")
|
||
cursor.execute("CREATE INDEX IF NOT EXISTS idx_user_risk_scores_updated_at ON user_risk_scores(updated_at)")
|
||
|
||
cursor.execute(
|
||
"""
|
||
CREATE TABLE IF NOT EXISTS ip_blacklist (
|
||
ip TEXT PRIMARY KEY,
|
||
reason TEXT,
|
||
is_active INTEGER DEFAULT 1,
|
||
added_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||
expires_at TIMESTAMP
|
||
)
|
||
"""
|
||
)
|
||
cursor.execute("CREATE INDEX IF NOT EXISTS idx_ip_blacklist_active ON ip_blacklist(is_active)")
|
||
cursor.execute("CREATE INDEX IF NOT EXISTS idx_ip_blacklist_expires ON ip_blacklist(expires_at)")
|
||
|
||
cursor.execute(
|
||
"""
|
||
CREATE TABLE IF NOT EXISTS threat_signatures (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
name TEXT UNIQUE NOT NULL,
|
||
threat_type TEXT NOT NULL,
|
||
pattern TEXT NOT NULL,
|
||
pattern_type TEXT DEFAULT 'regex',
|
||
score INTEGER DEFAULT 0,
|
||
is_active INTEGER DEFAULT 1,
|
||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||
)
|
||
"""
|
||
)
|
||
cursor.execute("CREATE INDEX IF NOT EXISTS idx_threat_signatures_type ON threat_signatures(threat_type)")
|
||
cursor.execute("CREATE INDEX IF NOT EXISTS idx_threat_signatures_active ON threat_signatures(is_active)")
|
||
|
||
conn.commit()
|
||
|
||
|
||
def _migrate_to_v14(conn):
|
||
"""迁移到版本14 - 安全防护:用户黑名单表"""
|
||
cursor = conn.cursor()
|
||
|
||
cursor.execute(
|
||
"""
|
||
CREATE TABLE IF NOT EXISTS user_blacklist (
|
||
user_id INTEGER PRIMARY KEY,
|
||
reason TEXT,
|
||
is_active INTEGER DEFAULT 1,
|
||
added_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||
expires_at TIMESTAMP
|
||
)
|
||
"""
|
||
)
|
||
cursor.execute("CREATE INDEX IF NOT EXISTS idx_user_blacklist_active ON user_blacklist(is_active)")
|
||
cursor.execute("CREATE INDEX IF NOT EXISTS idx_user_blacklist_expires ON user_blacklist(expires_at)")
|
||
|
||
conn.commit()
|