fix: 审核引擎优化 + 执行结果结构化展示

后端审核规则修复:
- Redis SEC-001: SET/DEL等常规命令不再误报AUTH要求,改为仅对管理级命令WARNING
- Redis SEC-003: 降级为INFO,排除已由CMD-001拦截的FLUSHDB/FLUSHALL
- Redis key提取: 排除CONFIG/ACL等管理命令,不再把子命令参数误认为key名
- MySQL SEC-006: 新增GRANT ALL/REVOKE ALL全局权限拦截
- MySQL SEC-007: 新增LOAD DATA/LOAD XML拦截
- MySQL SEC-008: 新增SLEEP()/BENCHMARK()时间盲注拦截
- MySQL SEC-009: 新增INTO OUTFILE/DUMPFILE文件写入拦截

前端执行结果结构化:
- TicketDetailPage执行结果从原始JSON改为结构化展示
- 解析JSON展示: 状态徽章+影响行数+耗时+验证状态
- Errors/Warnings逐条渲染: 红色错误框+黄色警告框+具体消息
- 删除废弃的executeStats/renderExecuteStats

其他: 审核规则6分组/慢查询移除/查询数据重命名/AuditLogs修复
测试: 1040 passed, TSC零错误
This commit is contained in:
logan committed 2026-05-24 01:31:33 +08:00
1 parent 9f56e866f2
commit 4ac2528f8e
19 files changed
+1202 -325

No files matched your search

+57
View File
@@ -5,6 +5,7 @@ Replaces mock aiomysql calls with actual driver connections.
"""
import asyncio
import time
import asyncio
import sqlite3
from typing import Optional, List, Dict, Any, Tuple
@@ -61,12 +62,68 @@ class DBConnector:
return await DBConnector._test_postgres(instance, start)
elif instance.db_type == "sqlite":
return await DBConnector._test_sqlite(instance, start)
elif instance.db_type == "redis":
return await DBConnector._test_redis(instance, start)
elif instance.db_type == "mongodb":
return await DBConnector._test_mongodb(instance, start)
else:
return ConnectionResult(False, f"暂不支持 {instance.db_type} 类型数据源")
except Exception as e:
elapsed = int((time.time() - start) * 1000)
return ConnectionResult(False, f"连接失败: {str(e)}", latency_ms=elapsed)
@staticmethod
async def _test_redis(instance: 'Instance', start: float) -> ConnectionResult:
"""Test Redis connectivity using synchronous redis client in thread."""
try:
import redis as redis_lib
def _connect():
client = redis_lib.Redis(
host=instance.host,
port=instance.port,
password=instance.password if instance.password and instance.password != "default" else None,
db=0,
socket_connect_timeout=5,
socket_timeout=5,
)
info = client.info("server")
version = info.get("redis_version", "unknown")
client.close()
return version
version = await asyncio.to_thread(_connect)
elapsed = int((time.time() - start) * 1000)
return ConnectionResult(True, f"Redis {version} 连接成功", version=version, latency_ms=elapsed)
except Exception as e:
elapsed = int((time.time() - start) * 1000)
return ConnectionResult(False, f"Redis 连接失败: {str(e)}", latency_ms=elapsed)
@staticmethod
async def _test_mongodb(instance: 'Instance', start: float) -> ConnectionResult:
"""Test MongoDB connectivity using pymongo in thread."""
try:
from pymongo import MongoClient
def _connect():
if instance.username and instance.username != "default":
uri = f"mongodb://{instance.username}:{instance.password}@{instance.host}:{instance.port}/"
else:
uri = f"mongodb://{instance.host}:{instance.port}/"
client = MongoClient(uri, serverSelectionTimeoutMS=5000, connectTimeoutMS=5000)
info = client.server_info()
version = info.get("version", "unknown")
client.close()
return version
version = await asyncio.to_thread(_connect)
elapsed = int((time.time() - start) * 1000)
return ConnectionResult(True, f"MongoDB {version} 连接成功", version=version, latency_ms=elapsed)
except Exception as e:
elapsed = int((time.time() - start) * 1000)
return ConnectionResult(False, f"MongoDB 连接失败: {str(e)}", latency_ms=elapsed)
@staticmethod
async def execute_query(
instance: Instance, database: Optional[str], sql: str, user_id: int
+100 -2
View File
@@ -2090,12 +2090,110 @@ def check_mysql_sec_005(sql_clean: str, sql_upper: str, report: AuditReportV2, r
# ═══════════════════════════════════════════════════
# 模块总结
# ── SEC-006: 权限相关高危语句拦截 ──
@register_rule(AuditRuleV2(
code="MYSQL-SEC-006",
name="DangerousPrivilegeStatement",
category=RuleCategory.SECURITY,
severity=Severity.ERROR,
db_type="mysql",
description="拦截 GRANT ALL / REVOKE ALL 等全局权限变更操作",
config={"enabled": True},
message_template="禁止执行 '{keyword}' 全局权限操作,请联系 DBA 通过权限管理流程申请",
))
def check_mysql_sec_006(sql_clean: str, sql_upper: str, report: AuditReportV2, rule: AuditRuleV2) -> None:
"""全局权限语句拦截。"""
# GRANT ALL / GRANT ALL PRIVILEGES
if re.search(r'\bGRANT\s+ALL\b', sql_upper):
report.findings.append(AuditFindingV2(
rule_code=rule.code, rule_name=rule.name, level=rule.severity,
message=rule.message_template.format(keyword="GRANT ALL"),
))
# REVOKE ALL
elif re.search(r'\bREVOKE\s+ALL\b', sql_upper):
report.findings.append(AuditFindingV2(
rule_code=rule.code, rule_name=rule.name, level=rule.severity,
message=rule.message_template.format(keyword="REVOKE ALL"),
))
# ── SEC-007: LOAD DATA / LOAD XML 拦截 ──
@register_rule(AuditRuleV2(
code="MYSQL-SEC-007",
name="BlockLoadData",
category=RuleCategory.SECURITY,
severity=Severity.ERROR,
db_type="mysql",
description="禁止 LOAD DATA / LOAD XML 操作,存在安全风险且可能导入大量数据",
config={"enabled": True},
message_template="禁止使用 '{keyword}',该操作存在安全风险,请使用 INSERT 批量导入",
))
def check_mysql_sec_007(sql_clean: str, sql_upper: str, report: AuditReportV2, rule: AuditRuleV2) -> None:
"""LOAD DATA/LOAD XML 拦截。"""
if re.search(r'\bLOAD\s+DATA\b', sql_upper):
report.findings.append(AuditFindingV2(
rule_code=rule.code, rule_name=rule.name, level=rule.severity,
message=rule.message_template.format(keyword="LOAD DATA"),
))
elif re.search(r'\bLOAD\s+XML\b', sql_upper):
report.findings.append(AuditFindingV2(
rule_code=rule.code, rule_name=rule.name, level=rule.severity,
message=rule.message_template.format(keyword="LOAD XML"),
))
# ── SEC-008: SLEEP / BENCHMARK 注入拦截 ──
@register_rule(AuditRuleV2(
code="MYSQL-SEC-008",
name="BlockTimeBasedInjection",
category=RuleCategory.SECURITY,
severity=Severity.ERROR,
db_type="mysql",
description="拦截 SLEEP() / BENCHMARK() 等时间盲注函数",
config={"enabled": True},
message_template="禁止使用 '{func}()' 函数,疑似时间盲注攻击",
))
def check_mysql_sec_008(sql_clean: str, sql_upper: str, report: AuditReportV2, rule: AuditRuleV2) -> None:
"""时间盲注函数拦截。"""
if re.search(r'\bSLEEP\s*\(', sql_upper):
report.findings.append(AuditFindingV2(
rule_code=rule.code, rule_name=rule.name, level=rule.severity,
message=rule.message_template.format(func="SLEEP"),
))
if re.search(r'\bBENCHMARK\s*\(', sql_upper):
report.findings.append(AuditFindingV2(
rule_code=rule.code, rule_name=rule.name, level=rule.severity,
message=rule.message_template.format(func="BENCHMARK"),
))
# ── SEC-009: INTO OUTFILE / INTO DUMPFILE 拦截 ──
@register_rule(AuditRuleV2(
code="MYSQL-SEC-009",
name="BlockIntoOutfile",
category=RuleCategory.SECURITY,
severity=Severity.ERROR,
db_type="mysql",
description="禁止 INTO OUTFILE / INTO DUMPFILE,防止写入 WebShell 或导出敏感数据",
config={"enabled": True},
message_template="禁止使用 '{keyword}',该操作可写入文件系统,存在严重安全风险",
))
def check_mysql_sec_009(sql_clean: str, sql_upper: str, report: AuditReportV2, rule: AuditRuleV2) -> None:
"""INTO OUTFILE/DUMPFILE 拦截。"""
if re.search(r'\bINTO\s+(OUT|DUMP)FILE\b', sql_upper):
keyword = "INTO OUTFILE" if "OUTFILE" in sql_upper else "INTO DUMPFILE"
report.findings.append(AuditFindingV2(
rule_code=rule.code, rule_name=rule.name, level=rule.severity,
message=rule.message_template.format(keyword=keyword),
))
# ═══════════════════════════════════════════════════
# 总计 63 条规则:
# 总计 67 条规则:
# - DDL 规则: 36 条 (MYSQL-DDL-001 ~ MYSQL-DDL-036)
# - DML 规则: 9 条 (MYSQL-DML-001 ~ MYSQL-DML-009)
# - Online-DDL 规则: 3 条 (MYSQL-OSC-001 ~ MYSQL-OSC-003)
# - 命名规则: 5 条 (MYSQL-NAM-001 ~ MYSQL-NAM-005)
# - 性能规则: 5 条 (MYSQL-PERF-001 ~ MYSQL-PERF-005)
# - 安全规则: 5 条 (MYSQL-SEC-001 ~ MYSQL-SEC-005)
# - 安全规则: 9 条 (MYSQL-SEC-001 ~ MYSQL-SEC-009)
# ═══════════════════════════════════════════════════
+32 -26
View File
@@ -98,6 +98,17 @@ def _extract_key_from_command(command: str, args: List[str]) -> Optional[str]:
if command in second_key_commands and len(args) >= 1:
return args[0]
# 管理命令/无 key 命令:不应提取 key
no_key_commands = {
"CONFIG", "ACL", "AUTH", "INFO", "PING", "FLUSHDB", "FLUSHALL",
"DBSIZE", "BGSAVE", "BGREWRITEAOF", "SAVE", "LASTSAVE",
"SHUTDOWN", "SLAVEOF", "REPLICAOF", "SLOWLOG", "MONITOR",
"DEBUG", "OBJECT", "CLIENT", "CLUSTER", "SENTINEL",
"MODULE", "COMMAND", "MEMORY", "LATENCY", "WAIT",
}
if command in no_key_commands:
return None
# 默认:第一个参数是 key
return args[0]
@@ -1610,35 +1621,28 @@ def check_redis_perf_008(sql_clean: str, sql_upper: str, report: AuditReportV2,
@register_rule(AuditRuleV2(
code="REDIS-SEC-001",
name="必须认证才能执行命令",
name="敏感管理命令需要二次确认",
category=RuleCategory.SECURITY,
severity=Severity.ERROR,
severity=Severity.WARNING,
db_type="redis",
description="非只读命令必须执行 AUTH 认证后才能执行",
config={"require_auth": True, "allowed_cmds_without_auth": ["PING", "AUTH", "ECHO", "QUIT", "HELLO"]},
message_template="命令 '{cmd}' 需要认证后才能执行,请先执行 AUTH password 进行认证",
description="CONFIG/ACL/SHUTDOWN 等管理级命令属于高危操作,建议限制执行",
config={"admin_cmds": ["CONFIG", "ACL", "SHUTDOWN", "DEBUG", "MODULE", "REPLICAOF", "SLAVEOF", "SAVE", "BGSAVE", "BGREWRITEAOF"]},
message_template="命令 '{cmd}' 属于 Redis 管理级操作,请确认业务必要性后再执行",
))
def check_redis_sec_001(sql_clean: str, sql_upper: str, report: AuditReportV2, rule: AuditRuleV2) -> None:
"""必须认证检查。
"""管理级命令检查。
提示执行敏感命令前需要 AUTH 认证。
仅对 CONFIG/ACL/SHUTDOWN 等管理级命令产生警告,
常规数据操作命令(SET/DEL/HSET 等)不触发——
平台已通过实例配置管理认证,无需额外提示 AUTH。
"""
command, args = _parse_redis_command(sql_clean)
if not command:
return
cmd = command.upper()
allowed = rule.config.get("allowed_cmds_without_auth", ["PING", "AUTH", "ECHO", "QUIT", "HELLO"])
if cmd in allowed:
return
# 只对写入和敏感命令要求认证
sensitive_cmds = {
"SET", "DEL", "FLUSHDB", "FLUSHALL", "CONFIG", "ACL", "RENAME",
"MIGRATE", "RESTORE", "SHUTDOWN", "DEBUG", "MODULE", "REPLICAOF",
"SLAVEOF", "SAVE", "BGSAVE", "BGREWRITEAOF",
}
if cmd in sensitive_cmds:
admin_cmds = rule.config.get("admin_cmds", [])
if cmd in admin_cmds:
report.findings.append(AuditFindingV2(
rule_code=rule.code,
rule_name=rule.name,
@@ -1696,25 +1700,27 @@ def check_redis_sec_002(sql_clean: str, sql_upper: str, report: AuditReportV2, r
@register_rule(AuditRuleV2(
code="REDIS-SEC-003",
name="危险命令必须被重命名",
name="危险命令重命名建议",
category=RuleCategory.SECURITY,
severity=Severity.WARNING,
severity=Severity.INFO,
db_type="redis",
description="建议将危险命令(FLUSHDB/FLUSHALL/CONFIG/DEBUG)通过 rename-command 重命名",
config={"dangerous_commands": ["FLUSHDB", "FLUSHALL", "CONFIG", "DEBUG", "SHUTDOWN"]},
message_template="危险命令 '{cmd}' 应通过 rename-command 在配置中重命名,防止误操作或恶意执行",
description="建议将 FLUSHDB/FLUSHALL/DEBUG/SHUTDOWN 通过 rename-command 重命名",
config={"dangerous_commands": ["DEBUG", "SHUTDOWN"]},
message_template="建议将 '{cmd}' 通过 rename-command 重命名,防止误操作",
))
def check_redis_sec_003(sql_clean: str, sql_upper: str, report: AuditReportV2, rule: AuditRuleV2) -> None:
"""危险命令重命名检查。
"""危险命令重命名建议。
当检测到危险命令使用时,提示应在配置中重命名。
FLUSHDB/FLUSHALL 已由 CMD-001 硬拦截,此处仅对 DEBUG/SHUTDOWN
等管理命令给出 rename-command 配置建议,避免与 CMD-001 重复。
"""
command, args = _parse_redis_command(sql_clean)
if not command:
return
cmd = command.upper()
dangerous = rule.config.get("dangerous_commands", ["FLUSHDB", "FLUSHALL", "CONFIG", "DEBUG", "SHUTDOWN"])
# 排除已由 CMD-001 拦截的 FLUSHDB/FLUSHALL/CONFIG
dangerous = rule.config.get("dangerous_commands", ["DEBUG", "SHUTDOWN"])
if cmd in dangerous:
report.findings.append(AuditFindingV2(
rule_code=rule.code,