feat: Phase1安全基线 — 风险等级+审核超时+执行进度+通知脱敏+高危确认+维护窗口 (1119测试)
新增功能:
- ticket_sql_items模型: 逐条SQL执行记录(状态/耗时/影响行数)
- risk_evaluator.py: 4级风险评估(low/medium/high/critical), 支持MySQL/PG/Redis/Mongo
- Ticket模型: 新增risk_level + workflow_id字段
- 审核引擎签名改造: audit(sql, db_type, instance_id=None) 支持EXPLAIN预检
- 审核超时保护: 10s超时跳过审核, 防止工单创建阻塞
- 工单创建自动风险计算 + 审批流匹配(instance.workflow_id优先)
- SQL items查询API: GET /{ticket_id}/sql-items 含进度统计
- 执行器集成: 执行完成后自动更新SQL items逐条状态
- 通知脱敏: password/secret/token自动掩码, SQL预览截断100字符
- 高危操作强制二次确认: high/critical级别需confirmed=true
- 维护窗口控制: system_settings配置执行时间窗口(支持跨午夜)
- 45个新测试: 风险评估24+通知脱敏5+维护窗口2+SQL分类11+配置2
This commit is contained in:
1 parent
030f3f2339
commit
7be0bf1a7c
9 files changed
+809
-16
No files matched your search
@@ -116,6 +116,7 @@ class AuditReportV2:
|
||||
sql: str
|
||||
findings: List[AuditFindingV2] = field(default_factory=list)
|
||||
db_type: str = "mysql"
|
||||
extra_info: Dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
@property
|
||||
def passed(self) -> bool:
|
||||
@@ -1633,7 +1634,9 @@ class SQLAuditEngineV2:
|
||||
return rules
|
||||
return [(r, f) for r, f in rules if r.db_type in ("all", db_type)]
|
||||
|
||||
async def audit(self, sql: str, db_type: str = "mysql") -> AuditReportV2:
|
||||
async def audit(
|
||||
self, sql: str, db_type: str = "mysql", instance_id: Optional[int] = None
|
||||
) -> AuditReportV2:
|
||||
"""执行 SQL 审核。
|
||||
|
||||
根据 SQL 语句类型,匹配对应的审核规则进行检查。
|
||||
@@ -1641,6 +1644,7 @@ class SQLAuditEngineV2:
|
||||
Args:
|
||||
sql: 待审核的 SQL 语句
|
||||
db_type: 目标数据库类型 (mysql/postgres/redis)
|
||||
instance_id: 目标实例 ID(用于 EXPLAIN 预检、上下文感知规则等)
|
||||
|
||||
Returns:
|
||||
AuditReportV2 审核报告
|
||||
@@ -1649,14 +1653,24 @@ class SQLAuditEngineV2:
|
||||
sql_upper = sql_clean.upper()
|
||||
|
||||
report = AuditReportV2(sql=sql_clean, db_type=db_type)
|
||||
report.extra_info["instance_id"] = instance_id
|
||||
|
||||
# 加载规则
|
||||
rules = await self._load_rules(db_type)
|
||||
|
||||
# 构建上下文(用于上下文感知规则)
|
||||
context: Dict[str, Any] = {"instance_id": instance_id, "db_session": self.db_session}
|
||||
|
||||
# 执行每条规则的检查
|
||||
for rule, check_func in rules:
|
||||
try:
|
||||
check_func(sql_clean, sql_upper, report, rule)
|
||||
check_func(sql_clean, sql_upper, report, rule, context=context)
|
||||
except TypeError:
|
||||
# 兼容旧签名 (sql, sql_upper, report, rule)
|
||||
try:
|
||||
check_func(sql_clean, sql_upper, report, rule)
|
||||
except Exception:
|
||||
continue
|
||||
except Exception:
|
||||
# 规则检查失败不影响整体审核
|
||||
continue
|
||||
|
||||
@@ -15,6 +15,37 @@ from urllib.parse import quote
|
||||
class NotificationSender:
|
||||
"""通知发送器,支持钉钉、飞书、邮件。"""
|
||||
|
||||
# 敏感字段关键词(用于通知脱敏)
|
||||
_SENSITIVE_PATTERNS = [
|
||||
(r"password\s*=\s*'[^']*'", "password='***'"),
|
||||
(r"password\s*=\s*\"[^\"]*\"", 'password="***"'),
|
||||
(r"secret\s*=\s*'[^']*'", "secret='***'"),
|
||||
(r"token\s*=\s*'[^']*'", "token='***'"),
|
||||
(r"DROP\s+TABLE\s+\w+", "DROP TABLE ***"),
|
||||
(r"TRUNCATE\s+TABLE?\s*\w+", "TRUNCATE ***"),
|
||||
(r"FLUSHALL", "FLUSHALL"),
|
||||
(r"FLUSHDB", "FLUSHDB"),
|
||||
]
|
||||
|
||||
@staticmethod
|
||||
def _mask_sensitive_data(data: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""对通知数据中的敏感字段进行脱敏。"""
|
||||
import re
|
||||
masked = dict(data)
|
||||
# sql_preview 截断到前100字符 + 脱敏
|
||||
if "sql_preview" in masked and masked["sql_preview"]:
|
||||
preview = str(masked["sql_preview"])[:100]
|
||||
for pattern, replacement in NotificationSender._SENSITIVE_PATTERNS:
|
||||
preview = re.sub(pattern, replacement, preview, flags=re.IGNORECASE)
|
||||
masked["sql_preview"] = preview + ("..." if len(str(data.get("sql_preview", ""))) > 100 else "")
|
||||
# error 字段脱敏
|
||||
if "error" in masked and masked["error"]:
|
||||
err = str(masked["error"])[:200]
|
||||
for pattern, replacement in NotificationSender._SENSITIVE_PATTERNS:
|
||||
err = re.sub(pattern, replacement, err, flags=re.IGNORECASE)
|
||||
masked["error"] = err
|
||||
return masked
|
||||
|
||||
# 内置消息模板
|
||||
TEMPLATES = {
|
||||
"ticket_created": {
|
||||
@@ -91,6 +122,9 @@ class NotificationSender:
|
||||
event_type: 事件类型
|
||||
data: 模板数据
|
||||
"""
|
||||
# 通知脱敏:隐藏 SQL 中的敏感内容
|
||||
data = self._mask_sensitive_data(data)
|
||||
|
||||
try:
|
||||
if channel_type == "dingtalk":
|
||||
return await self._send_dingtalk(config, event_type, data)
|
||||
|
||||
@@ -0,0 +1,189 @@
|
||||
"""工单风险等级自动评估。
|
||||
|
||||
根据 SQL 类型 + 操作对象 + 审核结果,自动计算 risk_level:
|
||||
- low: SELECT / 简单 DML (有 WHERE + LIMIT)
|
||||
- medium: 普通 DML / 低风险 DDL (CREATE TABLE)
|
||||
- high: 大表 ALTER / 无 WHERE 的 UPDATE/DELETE / 多表 JOIN
|
||||
- critical: DROP TABLE / TRUNCATE / FLUSHDB / FLUSHALL / dropDatabase
|
||||
|
||||
评估时机:工单创建时自动计算,写入 ticket.risk_level。
|
||||
"""
|
||||
|
||||
import re
|
||||
from typing import Optional
|
||||
|
||||
|
||||
def evaluate_risk_level(
|
||||
sql_content: str,
|
||||
db_type: str = "mysql",
|
||||
sql_type: str = "",
|
||||
audit_findings: Optional[list] = None,
|
||||
statement_count: int = 1,
|
||||
) -> str:
|
||||
"""评估 SQL 的风险等级。
|
||||
|
||||
Args:
|
||||
sql_content: SQL 文本(可能包含多条语句)
|
||||
db_type: 数据库类型 mysql/postgres/redis/mongodb
|
||||
sql_type: 前端传入的 SQL 类型标注 DDL/DML/DQL 等
|
||||
audit_findings: 审核引擎返回的 findings 列表
|
||||
statement_count: SQL 语句条数
|
||||
|
||||
Returns:
|
||||
risk_level: low / medium / high / critical
|
||||
"""
|
||||
sql_upper = sql_content.strip().upper()
|
||||
|
||||
# ── Redis 特殊处理 ──
|
||||
if db_type == "redis":
|
||||
return _evaluate_redis_risk(sql_upper)
|
||||
|
||||
# ── MongoDB 特殊处理 ──
|
||||
if db_type == "mongodb":
|
||||
return _evaluate_mongo_risk(sql_upper)
|
||||
|
||||
# ── MySQL / PostgreSQL ──
|
||||
|
||||
# 1. critical 级别 — 不可逆危险操作
|
||||
critical_patterns = [
|
||||
r"\bDROP\s+TABLE\b",
|
||||
r"\bDROP\s+DATABASE\b",
|
||||
r"\bDROP\s+SCHEMA\b",
|
||||
r"\bTRUNCATE\s",
|
||||
]
|
||||
for pattern in critical_patterns:
|
||||
if re.search(pattern, sql_upper):
|
||||
return "critical"
|
||||
|
||||
# 2. high 级别 — 高风险操作
|
||||
high_indicators = 0
|
||||
|
||||
# ALTER 大操作(但不包括 ADD INDEX 这类较小操作)
|
||||
if re.search(r"\bALTER\s+TABLE\b", sql_upper):
|
||||
alter_high = [
|
||||
r"\bDROP\s+COLUMN\b",
|
||||
r"\bMODIFY\s+COLUMN\b",
|
||||
r"\bCHANGE\s+COLUMN\b",
|
||||
r"\bDROP\s+INDEX\b",
|
||||
r"\bDROP\s+PRIMARY\s+KEY\b",
|
||||
r"\bCONVERT\s+TO\s+CHARACTER\s+SET\b",
|
||||
r"\bRENAME\s+(TO|AS)\b",
|
||||
]
|
||||
for p in alter_high:
|
||||
if re.search(p, sql_upper):
|
||||
high_indicators += 1
|
||||
break
|
||||
|
||||
# 无 WHERE 的 UPDATE / DELETE
|
||||
if re.search(r"\bUPDATE\s", sql_upper) and not re.search(r"\bWHERE\b", sql_upper):
|
||||
high_indicators += 1
|
||||
if re.search(r"\bDELETE\s+FROM\b", sql_upper) and not re.search(r"\bWHERE\b", sql_upper):
|
||||
high_indicators += 1
|
||||
|
||||
# 大量语句(>5条)
|
||||
if statement_count > 5:
|
||||
high_indicators += 1
|
||||
|
||||
# 审核结果有 ERROR 级别
|
||||
if audit_findings:
|
||||
errors = [f for f in audit_findings if f.get("level") == "error"]
|
||||
if len(errors) >= 2:
|
||||
high_indicators += 1
|
||||
|
||||
if high_indicators > 0:
|
||||
return "high"
|
||||
|
||||
# 3. medium 级别 — 常规变更
|
||||
medium_types = {"DDL", "DML", "SET", "HSET", "HMSET"}
|
||||
if sql_type in medium_types:
|
||||
return "medium"
|
||||
|
||||
# 有 WHERE 但没有 LIMIT 的 DELETE
|
||||
if re.search(r"\bDELETE\s+FROM\b", sql_upper):
|
||||
if not re.search(r"\bLIMIT\b", sql_upper):
|
||||
return "medium"
|
||||
|
||||
# 任何 DDL
|
||||
ddl_keywords = ["CREATE ", "ALTER ", "RENAME ", "GRANT ", "REVOKE ", "CREATE INDEX", "CREATE UNIQUE"]
|
||||
for kw in ddl_keywords:
|
||||
if kw in sql_upper:
|
||||
return "medium"
|
||||
|
||||
# 4. low 级别 — 只读 / 安全操作
|
||||
return "low"
|
||||
|
||||
|
||||
def _evaluate_redis_risk(sql_upper: str) -> str:
|
||||
"""Redis 命令风险评估。"""
|
||||
critical_cmds = ["FLUSHDB", "FLUSHALL"]
|
||||
for cmd in critical_cmds:
|
||||
if sql_upper.startswith(cmd):
|
||||
return "critical"
|
||||
|
||||
# 危险写操作
|
||||
high_cmds = ["DEL ", "UNLINK ", "RENAME ", "FLUSHDB", "FLUSHALL"]
|
||||
for cmd in high_cmds:
|
||||
if sql_upper.startswith(cmd):
|
||||
return "high"
|
||||
|
||||
# 一般写操作
|
||||
write_cmds = ["SET ", "HSET ", "HMSET ", "LPUSH ", "RPUSH ", "SADD ", "ZADD ",
|
||||
"INCR", "DECR", "EXPIRE ", "PEXPIRE "]
|
||||
for cmd in write_cmds:
|
||||
if sql_upper.startswith(cmd):
|
||||
return "medium"
|
||||
|
||||
# 只读命令
|
||||
return "low"
|
||||
|
||||
|
||||
def _evaluate_mongo_risk(sql_upper: str) -> str:
|
||||
"""MongoDB 操作风险评估。"""
|
||||
sql_lower = sql_upper.lower()
|
||||
critical_ops = ["dropdatabase", "drop database"]
|
||||
for op in critical_ops:
|
||||
if op in sql_lower:
|
||||
return "critical"
|
||||
|
||||
high_ops = ["drop(", "dropcollection", "remove(", "deletemany"]
|
||||
for op in high_ops:
|
||||
if op in sql_lower:
|
||||
return "high"
|
||||
|
||||
write_ops = ["insertone", "insertmany", "updateone", "updatemany", "deleteone",
|
||||
"createindex", "create_collection"]
|
||||
for op in write_ops:
|
||||
if op in sql_lower:
|
||||
return "medium"
|
||||
|
||||
return "low"
|
||||
|
||||
|
||||
# ── 风险等级配置(用于前端展示 / 审批流匹配)──
|
||||
|
||||
RISK_LEVEL_CONFIG = {
|
||||
"low": {
|
||||
"label": "低风险",
|
||||
"color": "#52c41a",
|
||||
"description": "只读查询 / 安全操作",
|
||||
"min_approvers": 1,
|
||||
},
|
||||
"medium": {
|
||||
"label": "中风险",
|
||||
"color": "#faad14",
|
||||
"description": "常规数据变更 / DDL 操作",
|
||||
"min_approvers": 1,
|
||||
},
|
||||
"high": {
|
||||
"label": "高风险",
|
||||
"color": "#fa8c16",
|
||||
"description": "无 WHERE 删除/更新 / 大表 ALTER / 批量操作",
|
||||
"min_approvers": 2,
|
||||
},
|
||||
"critical": {
|
||||
"label": "极高风险",
|
||||
"color": "#f5222d",
|
||||
"description": "DROP TABLE / TRUNCATE / FLUSHDB 等不可逆操作",
|
||||
"min_approvers": 2,
|
||||
},
|
||||
}
|
||||
Reference in new issue
Block a user