"""SQL Audit Engine — rule-based SQL review for tickets. Checks SQL against configurable rules and returns structured results: - level: "error" (must fix), "warning" (should review), "info" (FYI) - rule: rule identifier - message: human-readable description Supports: - MySQL / PostgreSQL SQL audit - Redis command audit - MongoDB operation audit - Dynamic rule loading from configuration - Risk level assessment (LOW / MEDIUM / HIGH / CRITICAL) - Weighted scoring algorithm """ import json import logging import re from dataclasses import dataclass, field from enum import Enum from typing import Any, Callable, Dict, List, Optional, Protocol logger = logging.getLogger(__name__) class Severity(str, Enum): ERROR = "error" WARNING = "warning" INFO = "info" class RiskLevel(str, Enum): """风险等级评估。""" LOW = "low" # 低风险,一般建议 MEDIUM = "medium" # 中风险,需要关注 HIGH = "high" # 高风险,强烈建议修改 CRITICAL = "critical" # 严重风险,必须修改 @dataclass class AuditFinding: rule: str level: Severity message: str risk_level: RiskLevel = RiskLevel.MEDIUM # 新增:风险等级 suggestion: str = "" # 新增:修改建议 category: str = "general" # 新增:规则分类 def to_dict(self) -> dict: return { "rule": self.rule, "level": self.level.value, "message": self.message, "risk_level": self.risk_level.value, "suggestion": self.suggestion, "category": self.category, } @dataclass class AuditReport: sql: str findings: List[AuditFinding] = field(default_factory=list) db_type: str = "mysql" risk_level: RiskLevel = RiskLevel.LOW risk_score: int = 0 @property def passed(self) -> bool: return not any(f.level == Severity.ERROR for f in self.findings) @property def has_warnings(self) -> bool: return any(f.level == Severity.WARNING for f in self.findings) @property def score(self) -> int: """加权评分算法: 100 = clean, error扣10-30分, warning扣5-15分, info扣1-3分。 根据风险等级调整扣分权重: - CRITICAL: error扣30分 - HIGH: error扣20分, warning扣15分 - MEDIUM: error扣10分, warning扣5分 - LOW: info扣1-3分 """ s = 100 for f in self.findings: if f.risk_level == RiskLevel.CRITICAL: if f.level == Severity.ERROR: s -= 30 elif f.level == Severity.WARNING: s -= 15 elif f.level == Severity.INFO: s -= 5 elif f.risk_level == RiskLevel.HIGH: if f.level == Severity.ERROR: s -= 20 elif f.level == Severity.WARNING: s -= 10 elif f.level == Severity.INFO: s -= 3 elif f.risk_level == RiskLevel.MEDIUM: if f.level == Severity.ERROR: s -= 10 elif f.level == Severity.WARNING: s -= 5 elif f.level == Severity.INFO: s -= 2 else: # LOW if f.level == Severity.ERROR: s -= 5 elif f.level == Severity.WARNING: s -= 3 elif f.level == Severity.INFO: s -= 1 return max(0, s) def to_dict(self) -> dict: return { "passed": self.passed, "score": self.score, "db_type": self.db_type, "risk_level": self.risk_level.value, "risk_score": self.risk_score, "findings": [f.to_dict() for f in self.findings], } def calculate_risk_level(self) -> RiskLevel: """根据发现的问题计算整体风险等级。""" if any(f.level == Severity.ERROR and f.risk_level == RiskLevel.CRITICAL for f in self.findings): return RiskLevel.CRITICAL if any(f.level == Severity.ERROR and f.risk_level == RiskLevel.HIGH for f in self.findings): return RiskLevel.HIGH if any(f.level == Severity.ERROR for f in self.findings): return RiskLevel.HIGH critical_warnings = sum( 1 for f in self.findings if f.level == Severity.WARNING and f.risk_level in (RiskLevel.CRITICAL, RiskLevel.HIGH) ) if critical_warnings >= 2: return RiskLevel.HIGH total_warnings = sum(1 for f in self.findings if f.level == Severity.WARNING) if total_warnings >= 3: return RiskLevel.MEDIUM if self.findings: return RiskLevel.LOW return RiskLevel.LOW # ===== 规则配置协议 ===== class AuditRule(Protocol): """审核规则协议。""" rule_code: str category: str severity: Severity risk_level: RiskLevel description: str def check(self, content: str, upper: str, report: AuditReport) -> None: """执行规则检查。""" ... # ===== 具体规则类 ===== @dataclass class SQLAuditRule: """SQL审核规则基类。""" rule_code: str category: str severity: Severity risk_level: RiskLevel description: str suggestion: str = "" enabled: bool = True def check(self, content: str, upper: str, report: AuditReport) -> None: pass class PatternAuditRule(SQLAuditRule): """基于正则表达式模式的审核规则。""" def __init__( self, rule_code: str, category: str, severity: Severity, risk_level: RiskLevel, description: str, suggestion: str, pattern: str, exclude_pattern: Optional[str] = None, ): super().__init__( rule_code=rule_code, category=category, severity=severity, risk_level=risk_level, description=description, suggestion=suggestion, ) self.pattern = re.compile(pattern, re.IGNORECASE) self.exclude_pattern = re.compile(exclude_pattern, re.IGNORECASE) if exclude_pattern else None def check(self, content: str, upper: str, report: AuditReport) -> None: if not self.enabled: return if self.pattern.search(upper): if self.exclude_pattern and self.exclude_pattern.search(upper): return report.findings.append(AuditFinding( rule=self.rule_code, level=self.severity, message=self.description, risk_level=self.risk_level, suggestion=self.suggestion, category=self.category, )) # ===== MySQL 审核规则集 ===== MYSQL_AUDIT_RULES: List[SQLAuditRule] = [ PatternAuditRule( rule_code="DROP_TABLE_DANGER", category="ddl", severity=Severity.WARNING, risk_level=RiskLevel.CRITICAL, description="DROP TABLE 操作不可逆,请确认已备份", suggestion="使用 DROP TABLE IF EXISTS 并在操作前备份数据", pattern=r"\bDROP\s+TABLE\b", ), PatternAuditRule( rule_code="TRUNCATE_DANGER", category="ddl", severity=Severity.ERROR, risk_level=RiskLevel.CRITICAL, description="TRUNCATE 会清空表数据且不可回滚", suggestion="使用 DELETE 配合 WHERE 子句替代,或使用事务", pattern=r"\bTRUNCATE\b", ), PatternAuditRule( rule_code="UPDATE_NO_WHERE", category="dml", severity=Severity.ERROR, risk_level=RiskLevel.CRITICAL, description="UPDATE 缺少 WHERE 条件,将更新全表", suggestion="添加 WHERE 条件限制更新范围", pattern=r"\bUPDATE\s+\w+\s+SET\b(?!.*\bWHERE\b)", ), PatternAuditRule( rule_code="DELETE_NO_WHERE", category="dml", severity=Severity.ERROR, risk_level=RiskLevel.CRITICAL, description="DELETE 缺少 WHERE 条件,将删除全表数据", suggestion="添加 WHERE 条件限制删除范围", pattern=r"\bDELETE\s+FROM\s+\w+\b(?!.*\bWHERE\b)", ), PatternAuditRule( rule_code="SELECT_STAR", category="query", severity=Severity.INFO, risk_level=RiskLevel.LOW, description="SELECT * 可能返回过多列,建议明确指定字段", suggestion="明确指定需要的字段名,减少网络传输和内存使用", pattern=r"SELECT\s+\*(?:\s|,|\))", ), PatternAuditRule( rule_code="DML_WITH_LIMIT", category="dml", severity=Severity.WARNING, risk_level=RiskLevel.HIGH, description="UPDATE/DELETE 使用 LIMIT 可能导致部分数据变更", suggestion="使用 WHERE 条件精确控制影响范围,避免 LIMIT", pattern=r"\b(UPDATE|DELETE)\b.*\bLIMIT\b", ), PatternAuditRule( rule_code="LARGE_IN_CLAUSE", category="query", severity=Severity.WARNING, risk_level=RiskLevel.MEDIUM, description="IN 子句包含过多值,性能较差", suggestion="使用临时表或 JOIN 替代大 IN 子句", pattern=r"\bIN\s*\([^)]{500,}\)", ), PatternAuditRule( rule_code="FULL_LIKE_SCAN", category="query", severity=Severity.INFO, risk_level=RiskLevel.LOW, description="LIKE '%...' 无法使用索引,大数据量下性能较差", suggestion="考虑使用全文索引或前缀匹配 LIKE 'prefix%'", pattern=r"\bLIKE\s+'%[^']*'", ), PatternAuditRule( rule_code="JOIN_NO_ON", category="query", severity=Severity.WARNING, risk_level=RiskLevel.HIGH, description="JOIN 缺少 ON 条件,可能产生笛卡尔积", suggestion="添加 ON 条件明确连接关系", pattern=r"\bJOIN\b", exclude_pattern=r"\b(NATURAL\s+JOIN|ON\b|CROSS\s+JOIN)", ), PatternAuditRule( rule_code="MULTI_STATEMENT", category="security", severity=Severity.ERROR, risk_level=RiskLevel.CRITICAL, description="单次提交不支持多条 SQL 语句(分号分隔)", suggestion="拆分为多个工单,每条语句单独提交", pattern=r";.*\b(SELECT|INSERT|UPDATE|DELETE|CREATE|ALTER|DROP)", ), PatternAuditRule( rule_code="SLEEP_INJECTION", category="security", severity=Severity.ERROR, risk_level=RiskLevel.CRITICAL, description="检测到时间延迟函数(SLEEP/BENCHMARK),可能存在注入风险", suggestion="移除时间延迟函数", pattern=r"\b(SLEEP|BENCHMARK)\s*\(", ), PatternAuditRule( rule_code="INTO_OUTFILE", category="security", severity=Severity.ERROR, risk_level=RiskLevel.CRITICAL, description="SELECT INTO OUTFILE/DUMPFILE 被禁止(文件写入风险)", suggestion="使用客户端工具导出数据", pattern=r"\bINTO\s+(OUTFILE|DUMPFILE)\b", ), PatternAuditRule( rule_code="PROCEDURE_CALL", category="security", severity=Severity.ERROR, risk_level=RiskLevel.HIGH, description="存储过程/函数调用可能存在安全风险", suggestion="避免使用存储过程,改用标准 SQL", pattern=r"\b(CALL|EXEC|EXECUTE)\b", ), PatternAuditRule( rule_code="UNION_INJECTION", category="security", severity=Severity.ERROR, risk_level=RiskLevel.CRITICAL, description="检测到 UNION 注入模式", suggestion="检查 SQL 来源合法性", pattern=r"\bUNION\s+(ALL\s+)?SELECT\b", ), ] # ===== PostgreSQL 审核规则集 ===== POSTGRES_AUDIT_RULES: List[SQLAuditRule] = [ PatternAuditRule( rule_code="PG_DROP_TABLE", category="ddl", severity=Severity.WARNING, risk_level=RiskLevel.CRITICAL, description="DROP TABLE 操作不可逆,请确认已备份", suggestion="使用 DROP TABLE IF EXISTS 并在操作前备份数据", pattern=r"\bDROP\s+TABLE\b", ), PatternAuditRule( rule_code="PG_TRUNCATE", category="ddl", severity=Severity.ERROR, risk_level=RiskLevel.CRITICAL, description="TRUNCATE 会清空表数据且不可回滚", suggestion="使用 DELETE 配合 WHERE 子句或事务", pattern=r"\bTRUNCATE\b", ), PatternAuditRule( rule_code="PG_UPDATE_NO_WHERE", category="dml", severity=Severity.ERROR, risk_level=RiskLevel.CRITICAL, description="UPDATE 缺少 WHERE 条件,将更新全表", suggestion="添加 WHERE 条件限制更新范围", pattern=r"\bUPDATE\s+\w+\s+SET\b(?!.*\bWHERE\b)", ), PatternAuditRule( rule_code="PG_DELETE_NO_WHERE", category="dml", severity=Severity.ERROR, risk_level=RiskLevel.CRITICAL, description="DELETE 缺少 WHERE 条件,将删除全表数据", suggestion="添加 WHERE 条件限制删除范围", pattern=r"\bDELETE\s+FROM\s+\w+\b(?!.*\bWHERE\b)", ), PatternAuditRule( rule_code="PG_SELECT_STAR", category="query", severity=Severity.INFO, risk_level=RiskLevel.LOW, description="SELECT * 可能返回过多列", suggestion="明确指定需要的字段名", pattern=r"SELECT\s+\*(?:\s|,|\))", ), PatternAuditRule( rule_code="PG_VACUUM_DANGER", category="maintenance", severity=Severity.WARNING, risk_level=RiskLevel.HIGH, description="VACUUM FULL 会锁定表并重建", suggestion="在低峰期执行 VACUUM FULL,或使用普通 VACUUM", pattern=r"\bVACUUM\s+FULL\b", ), PatternAuditRule( rule_code="PG_LOCK_TABLE", category="locking", severity=Severity.WARNING, risk_level=RiskLevel.HIGH, description="LOCK TABLE 会显式锁定表,可能导致阻塞", suggestion="使用事务隔离级别替代显式锁", pattern=r"\bLOCK\s+TABLE\b", ), PatternAuditRule( rule_code="PG_CREATE_EXTENSION", category="ddl", severity=Severity.WARNING, risk_level=RiskLevel.MEDIUM, description="CREATE EXTENSION 需要超级用户权限", suggestion="确认扩展安全性并由 DBA 执行", pattern=r"\bCREATE\s+EXTENSION\b", ), PatternAuditRule( rule_code="PG_COPY_FROM_FILE", category="security", severity=Severity.ERROR, risk_level=RiskLevel.CRITICAL, description="COPY FROM FILE 存在文件系统访问风险", suggestion="使用 COPY FROM STDIN 或客户端工具导入", pattern=r"\bCOPY\b.*\bFROM\s+(?!STDIN)", ), PatternAuditRule( rule_code="PG_MULTI_STATEMENT", category="security", severity=Severity.ERROR, risk_level=RiskLevel.CRITICAL, description="单次提交不支持多条 SQL 语句", suggestion="拆分为多个工单提交", pattern=r";.*\b(SELECT|INSERT|UPDATE|DELETE|CREATE|ALTER|DROP)", ), PatternAuditRule( rule_code="PG_SLEEP_INJECTION", category="security", severity=Severity.ERROR, risk_level=RiskLevel.CRITICAL, description="检测到时间延迟函数(PG_SLEEP),可能存在注入风险", suggestion="移除时间延迟函数", pattern=r"\bPG_SLEEP\s*\(", ), ] # ===== Redis 审核规则集 ===== REDIS_AUDIT_RULES: List[SQLAuditRule] = [ PatternAuditRule( rule_code="REDIS_FLUSH", category="danger", severity=Severity.ERROR, risk_level=RiskLevel.CRITICAL, description="FLUSHDB/FLUSHALL 会清空所有数据", suggestion="确认备份数据并在维护窗口执行", pattern=r"\b(FLUSHDB|FLUSHALL)\b", ), PatternAuditRule( rule_code="REDIS_KEY_DELETE", category="dml", severity=Severity.WARNING, risk_level=RiskLevel.HIGH, description="批量删除键(DEL KEYS)风险较高", suggestion="使用 UNLINK 异步删除,或使用 SCAN 分批删除", pattern=r"\bDEL\s+(?!\w+\b)", ), PatternAuditRule( rule_code="REDIS_SHUTDOWN", category="danger", severity=Severity.ERROR, risk_level=RiskLevel.CRITICAL, description="SHUTDOWN 会关闭 Redis 服务器", suggestion="通过运维面板操作服务器", pattern=r"\bSHUTDOWN\b", ), PatternAuditRule( rule_code="REDIS_CONFIG_SET", category="security", severity=Severity.ERROR, risk_level=RiskLevel.CRITICAL, description="CONFIG SET 可修改服务器配置", suggestion="通过配置文件修改并由 DBA 审核", pattern=r"\bCONFIG\s+SET\b", ), PatternAuditRule( rule_code="REDIS_REPLICAOF", category="danger", severity=Severity.ERROR, risk_level=RiskLevel.CRITICAL, description="REPLICAOF/SLAVEOF 会改变主从关系", suggestion="通过运维面板管理主从配置", pattern=r"\b(REPLICAOF|SLAVEOF)\b", ), PatternAuditRule( rule_code="REDIS_DEBUG", category="security", severity=Severity.ERROR, risk_level=RiskLevel.CRITICAL, description="DEBUG 命令可能导致数据损坏或信息泄露", suggestion="禁止在生产环境使用 DEBUG 命令", pattern=r"\bDEBUG\b", ), ] # ===== MongoDB 审核规则集 ===== MONGODB_AUDIT_RULES: List[SQLAuditRule] = [ PatternAuditRule( rule_code="MONGO_DROP_COLLECTION", category="ddl", severity=Severity.ERROR, risk_level=RiskLevel.CRITICAL, description="drop 操作会删除整个集合", suggestion="使用 deleteMany 删除文档保留集合", pattern=r'"operation"\s*:\s*"drop"', ), PatternAuditRule( rule_code="MONGO_DROP_INDEX", category="ddl", severity=Severity.WARNING, risk_level=RiskLevel.HIGH, description="dropIndex 会影响查询性能", suggestion="确认索引不再使用后再删除", pattern=r'"operation"\s*:\s*"dropIndex"', ), PatternAuditRule( rule_code="MONGO_DELETE_NO_FILTER", category="dml", severity=Severity.ERROR, risk_level=RiskLevel.CRITICAL, description="deleteMany 缺少 filter 将删除所有文档", suggestion="添加 filter 条件限制删除范围", pattern=r'"operation"\s*:\s*"deleteMany"(?!.*"filter"\s*:\s*\{[^}])', ), PatternAuditRule( rule_code="MONGO_UPDATE_NO_FILTER", category="dml", severity=Severity.ERROR, risk_level=RiskLevel.CRITICAL, description="updateMany 缺少 filter 将更新所有文档", suggestion="添加 filter 条件限制更新范围", pattern=r'"operation"\s*:\s*"updateMany"(?!.*"filter"\s*:\s*\{[^}])', ), PatternAuditRule( rule_code="MONGO_UNSET_FIELD", category="dml", severity=Severity.WARNING, risk_level=RiskLevel.MEDIUM, description="$unset 会删除字段,数据不可恢复", suggestion="确认字段确实不再需要", pattern=r'"\$unset"', ), PatternAuditRule( rule_code="MONGO_RENAME_COLLECTION", category="ddl", severity=Severity.WARNING, risk_level=RiskLevel.HIGH, description="renameCollection 会重命名集合", suggestion="确认新名称不会与现有集合冲突", pattern=r'"operation"\s*:\s*"renameCollection"', ), PatternAuditRule( rule_code="MONGO_JAVASCRIPT", category="security", severity=Severity.ERROR, risk_level=RiskLevel.CRITICAL, description="$where/$function 可执行任意 JavaScript", suggestion="使用标准查询操作符替代", pattern=r'"\$(where|function|accumulator)"', ), PatternAuditRule( rule_code="MONGO_OUT_STAGE", category="security", severity=Severity.ERROR, risk_level=RiskLevel.CRITICAL, description="aggregate $out/$merge 阶段会写入数据", suggestion="使用 find 查询替代", pattern=r'"\$(out|merge)"', ), ] # ===== 规则注册表 ===== RULE_REGISTRY: Dict[str, List[SQLAuditRule]] = { "mysql": MYSQL_AUDIT_RULES, "postgres": POSTGRES_AUDIT_RULES, "postgresql": POSTGRES_AUDIT_RULES, "redis": REDIS_AUDIT_RULES, "mongo": MONGODB_AUDIT_RULES, } class SQLAuditEngine: """增强的规则审核引擎。 支持多数据库类型的规则审核,动态规则加载,风险等级评估。 Attributes: env: 环境类型(development/production) db_type: 数据库类型 custom_rules: 自定义规则列表 """ def __init__(self, env: str = "development", db_type: str = "mysql"): self.env = env self.db_type = db_type.lower() self.custom_rules: List[SQLAuditRule] = [] def add_custom_rule(self, rule: SQLAuditRule) -> None: """添加自定义审核规则。 Args: rule: 自定义规则对象 """ self.custom_rules.append(rule) logger.info("添加自定义规则: %s", rule.rule_code) def load_rules_from_config(self, config: List[Dict[str, Any]]) -> None: """从配置加载自定义规则。 配置格式: [ { "rule_code": "RULE_NAME", "category": "security", "severity": "error", "risk_level": "critical", "description": "规则描述", "suggestion": "修改建议", "pattern": r"正则表达式", "exclude_pattern": r"排除正则" (可选) } ] Args: config: 规则配置列表 """ for cfg in config: try: severity = Severity(cfg.get("severity", "warning")) risk_level = RiskLevel(cfg.get("risk_level", "medium")) rule = PatternAuditRule( rule_code=cfg["rule_code"], category=cfg.get("category", "custom"), severity=severity, risk_level=risk_level, description=cfg["description"], suggestion=cfg.get("suggestion", ""), pattern=cfg["pattern"], exclude_pattern=cfg.get("exclude_pattern"), ) self.custom_rules.append(rule) logger.info("从配置加载规则: %s", rule.rule_code) except (KeyError, re.error) as e: logger.error("加载规则配置失败: %s, error: %s", cfg.get("rule_code", "unknown"), e) def audit(self, sql: str) -> AuditReport: """执行审核。 根据 db_type 选择对应的规则集,执行所有规则检查。 Args: sql: 要审核的 SQL/命令/JSON Returns: AuditReport: 审核报告 """ report = AuditReport(sql=sql, db_type=self.db_type) clean = sql.strip() upper = clean.upper() # 空内容检查 if not clean: report.findings.append(AuditFinding( rule="EMPTY_CONTENT", level=Severity.ERROR, message="审核内容不能为空", risk_level=RiskLevel.CRITICAL, category="validation", )) return report # 获取数据库类型对应的规则 rules = RULE_REGISTRY.get(self.db_type, []) # 执行内置规则 for rule in rules: if rule.enabled: rule.check(clean, upper, report) # 执行自定义规则 for rule in self.custom_rules: if rule.enabled: rule.check(clean, upper, report) # 环境特定规则 if self.env == "production": self._check_production_rules(clean, upper, report) # 计算风险等级 report.risk_level = report.calculate_risk_level() report.risk_score = report.score return report def _check_production_rules(self, clean: str, upper: str, report: AuditReport) -> None: """生产环境额外规则。 Args: clean: 原始内容 upper: 大写内容 report: 审核报告 """ if self.db_type in ("mysql", "postgres", "postgresql"): # ALTER TABLE 限制 if re.search(r"\bALTER\s+TABLE\b", upper): report.findings.append(AuditFinding( rule="ALTER_TABLE_PRODUCTION", level=Severity.ERROR, message="生产环境 ALTER TABLE 需 DBA 特别审批", risk_level=RiskLevel.HIGH, suggestion="在低峰期执行,使用 pt-online-schema-change 等在线工具", category="production", )) # CREATE INDEX 限制 if re.search(r"\bCREATE\s+(UNIQUE\s+)?INDEX\b", upper): report.findings.append(AuditFinding( rule="CREATE_INDEX_PRODUCTION", level=Severity.WARNING, message="生产环境创建索引需确认影响范围", risk_level=RiskLevel.MEDIUM, suggestion="使用 ONLINE/CONCURRENT 选项避免锁表", category="production", )) # 没有索引的 UPDATE/DELETE if re.search(r"\b(UPDATE|DELETE)\b", upper): report.findings.append(AuditFinding( rule="DML_PRODUCTION_CAUTION", level=Severity.WARNING, message="生产环境 UPDATE/DELETE 需要确认 WHERE 条件有索引覆盖", risk_level=RiskLevel.HIGH, suggestion="使用 EXPLAIN 确认执行计划使用了索引", category="production", )) @staticmethod def get_supported_rules(db_type: str) -> List[Dict[str, str]]: """获取指定数据库类型支持的所有规则列表。 Args: db_type: 数据库类型 Returns: 规则信息列表 """ rules = RULE_REGISTRY.get(db_type.lower(), []) return [ { "rule_code": r.rule_code, "category": r.category, "severity": r.severity.value, "risk_level": r.risk_level.value, "description": r.description, "suggestion": r.suggestion, } for r in rules if r.enabled ]