791 lines
27 KiB
Python
791 lines
27 KiB
Python
"""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
|
||
]
|