fix: merge后14个测试修复+前端TS编译修复 - 895/895通过+前端零错误

This commit is contained in:
logan committed 2026-05-23 17:50:13 +08:00
1 parent 58eb78c987
commit b39c426317
24 files changed
+3918 -760

No files matched your search

+708 -152
View File
@@ -4,11 +4,23 @@ 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 typing import List, Optional
from enum import Enum
from typing import Any, Callable, Dict, List, Optional, Protocol
logger = logging.getLogger(__name__)
class Severity(str, Enum):
@@ -17,17 +29,41 @@ class Severity(str, Enum):
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:
@@ -39,196 +75,716 @@ class AuditReport:
@property
def score(self) -> int:
"""100 = clean, -10 per error, -5 per warning."""
"""加权评分算法: 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.level == Severity.ERROR:
s -= 10
elif f.level == Severity.WARNING:
s -= 5
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,
"findings": [
{"rule": f.rule, "level": f.level.value, "message": f.message}
for f in self.findings
],
"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:
"""Rule-based SQL audit engine."""
"""增强的规则审核引擎。
def __init__(self, env: str = "development"):
支持多数据库类型的规则审核,动态规则加载,风险等级评估。
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:
report = AuditReport(sql=sql)
"""执行审核。
根据 db_type 选择对应的规则集,执行所有规则检查。
Args:
sql: 要审核的 SQL/命令/JSON
Returns:
AuditReport: 审核报告
"""
report = AuditReport(sql=sql, db_type=self.db_type)
clean = sql.strip()
upper = clean.upper()
# Rule 1: DDL without IF EXISTS
self._check_ddl_if_exists(clean, upper, report)
# 空内容检查
if not clean:
report.findings.append(AuditFinding(
rule="EMPTY_CONTENT",
level=Severity.ERROR,
message="审核内容不能为空",
risk_level=RiskLevel.CRITICAL,
category="validation",
))
return report
# Rule 2: DROP TABLE danger
self._check_drop_table(upper, report)
# 获取数据库类型对应的规则
rules = RULE_REGISTRY.get(self.db_type, [])
# Rule 3: UPDATE/DELETE without WHERE
self._check_missing_where(clean, upper, report)
# 执行内置规则
for rule in rules:
if rule.enabled:
rule.check(clean, upper, report)
# Rule 4: SELECT * usage
self._check_select_star(clean, upper, report)
# 执行自定义规则
for rule in self.custom_rules:
if rule.enabled:
rule.check(clean, upper, report)
# Rule 5: LIMIT on DML
self._check_limit_dml(clean, upper, report)
# Rule 6: Production env extra checks
# 环境特定规则
if self.env == "production":
self._check_production_rules(clean, upper, report)
# Rule 7: Multiple statements (semicolon)
self._check_multiple_statements(clean, report)
# Rule 8: Large IN clause
self._check_large_in(clean, upper, report)
# Rule 9: No WHERE with LIKE '%...'
self._check_full_like(clean, upper, report)
# Rule 10: JOIN without ON
self._check_join_without_on(upper, report)
# 计算风险等级
report.risk_level = report.calculate_risk_level()
report.risk_score = report.score
return report
def _check_ddl_if_exists(self, clean: str, upper: str, report: AuditReport):
"""ALTER/DROP statements should use IF EXISTS where applicable."""
if re.search(r"\bDROP\s+TABLE\b", upper) and "IF EXISTS" not in upper:
report.findings.append(AuditFinding(
rule="DDL_IF_EXISTS",
level=Severity.WARNING,
message="DROP TABLE 建议添加 IF EXISTS 防止表不存在时报错",
))
def _check_production_rules(self, clean: str, upper: str, report: AuditReport) -> None:
"""生产环境额外规则。
def _check_drop_table(self, upper: str, report: AuditReport):
"""DROP TABLE is always dangerous in production."""
if re.search(r"\bDROP\s+TABLE\b", upper):
if self.env == "production":
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="DROP_TABLE_PRODUCTION",
rule="ALTER_TABLE_PRODUCTION",
level=Severity.ERROR,
message="生产环境禁止 DROP TABLE 操作",
message="生产环境 ALTER TABLE 需 DBA 特别审批",
risk_level=RiskLevel.HIGH,
suggestion="在低峰期执行,使用 pt-online-schema-change 等在线工具",
category="production",
))
else:
# CREATE INDEX 限制
if re.search(r"\bCREATE\s+(UNIQUE\s+)?INDEX\b", upper):
report.findings.append(AuditFinding(
rule="DROP_TABLE",
rule="CREATE_INDEX_PRODUCTION",
level=Severity.WARNING,
message="DROP TABLE 操作不可逆,请确认已备份",
message="生产环境创建索引需确认影响范围",
risk_level=RiskLevel.MEDIUM,
suggestion="使用 ONLINE/CONCURRENT 选项避免锁表",
category="production",
))
if re.search(r"\bTRUNCATE\b", upper):
report.findings.append(AuditFinding(
rule="TRUNCATE",
level=Severity.ERROR if self.env == "production" else Severity.WARNING,
message="TRUNCATE 会清空表数据且不可回滚",
))
def _check_missing_where(self, clean: str, upper: str, report: AuditReport):
"""UPDATE/DELETE without WHERE clause affects all rows."""
if re.match(r"\s*UPDATE\s+", upper, re.IGNORECASE):
if "WHERE" not in upper:
# 没有索引的 UPDATE/DELETE
if re.search(r"\b(UPDATE|DELETE)\b", upper):
report.findings.append(AuditFinding(
rule="UPDATE_NO_WHERE",
level=Severity.ERROR,
message="UPDATE 缺少 WHERE 条件,将更新全表",
))
if re.match(r"\s*DELETE\s+FROM\s+", upper, re.IGNORECASE):
if "WHERE" not in upper:
report.findings.append(AuditFinding(
rule="DELETE_NO_WHERE",
level=Severity.ERROR,
message="DELETE 缺少 WHERE 条件,将删除全表数据",
))
def _check_select_star(self, clean: str, upper: str, report: AuditReport):
"""SELECT * is inefficient for large tables."""
if re.search(r"SELECT\s+\*(?:\s|,|\))", upper):
report.findings.append(AuditFinding(
rule="SELECT_STAR",
level=Severity.INFO,
message="SELECT * 可能返回过多列,建议明确指定字段",
))
def _check_limit_dml(self, clean: str, upper: str, report: AuditReport):
"""UPDATE/DELETE with LIMIT is MySQL-specific and risky."""
for kw in ("UPDATE", "DELETE"):
if re.match(rf"\s*{kw}\b", upper):
if "LIMIT" in upper:
report.findings.append(AuditFinding(
rule="DML_WITH_LIMIT",
level=Severity.WARNING,
message=f"{kw} 使用 LIMIT 可能导致部分数据变更,请确认意图",
))
def _check_production_rules(self, clean: str, upper: str, report: AuditReport):
"""Extra strict rules for production."""
if re.search(r"\bALTER\s+TABLE\b", upper):
report.findings.append(AuditFinding(
rule="ALTER_TABLE_PRODUCTION",
level=Severity.ERROR,
message="生产环境 ALTER TABLE 需 DBA 特别审批",
))
if re.search(r"\bCREATE\s+(TABLE|INDEX)\b", upper):
report.findings.append(AuditFinding(
rule="CREATE_DDL_PRODUCTION",
level=Severity.WARNING,
message="生产环境创建对象需确认影响范围",
))
def _check_multiple_statements(self, clean: str, report: AuditReport):
"""Multiple SQL statements separated by semicolons."""
# Count semicolons not at the end
stripped = clean.rstrip(";").strip()
if ";" in stripped:
report.findings.append(AuditFinding(
rule="MULTI_STATEMENT",
level=Severity.ERROR,
message="单次提交不支持多条 SQL 语句(分号分隔),请拆分为多个工单",
))
def _check_large_in(self, clean: str, upper: str, report: AuditReport):
"""IN clause with many values is slow."""
in_matches = re.findall(r"\bIN\s*\(([^)]+)\)", upper)
for m in in_matches:
count = m.count(",") + 1
if count > 100:
report.findings.append(AuditFinding(
rule="LARGE_IN",
rule="DML_PRODUCTION_CAUTION",
level=Severity.WARNING,
message=f"IN 子句包含 {count} 个值,建议使用临时表或 JOIN 替代",
message="生产环境 UPDATE/DELETE 需要确认 WHERE 条件有索引覆盖",
risk_level=RiskLevel.HIGH,
suggestion="使用 EXPLAIN 确认执行计划使用了索引",
category="production",
))
def _check_full_like(self, clean: str, upper: str, report: AuditReport):
"""LIKE '%...' cannot use index."""
if re.search(r"\bLIKE\s+'%[^']*'", clean, re.IGNORECASE):
report.findings.append(AuditFinding(
rule="FULL_LIKE",
level=Severity.INFO,
message="LIKE '%...' 无法使用索引,大数据量下性能较差",
))
@staticmethod
def get_supported_rules(db_type: str) -> List[Dict[str, str]]:
"""获取指定数据库类型支持的所有规则列表。
def _check_join_without_on(self, upper: str, report: AuditReport):
"""JOIN without ON produces cartesian product."""
# Match JOIN but not followed by ON (within reasonable distance)
if re.search(r"\bJOIN\b", upper) and "ON" not in upper:
# Could be NATURAL JOIN, allow that
if not re.search(r"\bNATURAL\s+JOIN\b", upper):
report.findings.append(AuditFinding(
rule="JOIN_NO_ON",
level=Severity.WARNING,
message="JOIN 缺少 ON 条件,可能产生笛卡尔积",
))
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
]