fix: merge后14个测试修复+前端TS编译修复 - 895/895通过+前端零错误
This commit is contained in:
1 parent
58eb78c987
commit
b39c426317
24 files changed
+3918
-760
No files matched your search
@@ -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
|
||||
]
|
||||
Reference in new issue
Block a user