Files
opsxsql/backend/app/services/redis_audit_rules.py
T
logan 4ac2528f8e fix: 审核引擎优化 + 执行结果结构化展示
后端审核规则修复:
- Redis SEC-001: SET/DEL等常规命令不再误报AUTH要求,改为仅对管理级命令WARNING
- Redis SEC-003: 降级为INFO,排除已由CMD-001拦截的FLUSHDB/FLUSHALL
- Redis key提取: 排除CONFIG/ACL等管理命令,不再把子命令参数误认为key名
- MySQL SEC-006: 新增GRANT ALL/REVOKE ALL全局权限拦截
- MySQL SEC-007: 新增LOAD DATA/LOAD XML拦截
- MySQL SEC-008: 新增SLEEP()/BENCHMARK()时间盲注拦截
- MySQL SEC-009: 新增INTO OUTFILE/DUMPFILE文件写入拦截

前端执行结果结构化:
- TicketDetailPage执行结果从原始JSON改为结构化展示
- 解析JSON展示: 状态徽章+影响行数+耗时+验证状态
- Errors/Warnings逐条渲染: 红色错误框+黄色警告框+具体消息
- 删除废弃的executeStats/renderExecuteStats

其他: 审核规则6分组/慢查询移除/查询数据重命名/AuditLogs修复
测试: 1040 passed, TSC零错误
2026-05-24 01:31:33 +08:00

1870 lines
72 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Redis 审核规则引擎 — 43+ 条 Redis 特有的审核规则。
为 SQL 审核平台提供完整的 Redis 命令审核能力,覆盖:
- KEY 管理规则 (10条): Key命名规范、大小限制、过期策略
- 命令安全规则 (12条): 危险命令检测(FLUSHDB/SHUTDOWN/CONFIG SET等)
- 数据结构规则 (8条): 各类型最佳实践与大小限制
- 性能规则 (8条): 大Key检测、批量操作限制、阻塞命令
- 安全规则 (5条): AUTH, ACL 配置与保护模式
使用方式:
engine = SQLAuditEngineV2(db_session)
report = await engine.audit("SET user:1001:name张三 EX3600", db_type="redis")
规则编码规范:
KEY: REDIS-KEY-001 ~ REDIS-KEY-010
CMD: REDIS-CMD-001 ~ REDIS-CMD-012
DS: REDIS-DS-001 ~ REDIS-DS-008
PERF: REDIS-PERF-001 ~ REDIS-PERF-008
SEC: REDIS-SEC-001 ~ REDIS-SEC-005
"""
from __future__ import annotations
import re
from typing import Any, Dict, List, Optional, Tuple
from app.services.audit_engine_v2 import (
AuditFindingV2,
AuditRuleV2,
AuditReportV2,
Severity,
RuleCategory,
register_rule,
)
# ═══════════════════════════════════════════════
# Redis 辅助解析函数
# ═══════════════════════════════════════════════
def _parse_redis_command(sql_clean: str) -> Tuple[str, List[str]]:
"""解析 Redis 命令,返回 (命令名大写, 参数列表)。
Args:
sql_clean: 单行 Redis 命令字符串
Returns:
(command_upper, args_list) 元组
例如: "SET key value EX 3600" -> ("SET", ["key", "value", "EX", "3600"])
"""
parts = sql_clean.strip().split()
if not parts:
return "", []
command = parts[0].upper()
args = parts[1:] if len(parts) > 1 else []
return command, args
def _extract_key_from_command(command: str, args: List[str]) -> Optional[str]:
"""从 Redis 命令中提取 Key 名称。
大多数命令的第一个参数是 Key,部分命令需要特殊处理。
Args:
command: 命令名(大写)
args: 参数列表
Returns:
Key 名称,如果无法提取则返回 None
"""
if not args:
return None
# 特殊命令:第二个参数才是 key
second_key_commands = {
"GETSET", "LPUSH", "RPUSH", "LPOP", "RPOP", "LINDEX", "LSET",
"LREM", "LINSERT", "SADD", "SREM", "SPOP", "SMOVE", "SISMEMBER",
"ZADD", "ZREM", "ZSCORE", "ZRANK", "ZREVRANK", "ZINCRBY",
"HSET", "HGET", "HDEL", "HINCRBY", "HINCRBYFLOAT", "HMSET",
"HMGET", "HSETNX", "HEXISTS", "HLEN", "HKEYS", "HVALS",
"HGETALL", "HSCAN", "HPOP", "SETEX", "PSETEX", "SETNX",
}
# 多 key 命令
multi_key_commands = {
"DEL", "UNLINK", "EXISTS", "TOUCH", "MGET", "MSET", "MSETNX",
"SDIFF", "SINTER", "SUNION", "SDIFFSTORE", "SINTERSTORE", "SUNIONSTORE",
"BLPOP", "BRPOP", "BRPOPLPUSH", "BLMOVE", "RPOPLPUSH", "LMOVE",
"RENAME", "RENAMENX", "SMOVE", "PFMERGE", "PFCOUNT",
"BZPOPMIN", "BZPOPMAX", "ZDIFF", "ZINTER", "ZUNION",
"ZDIFFSTORE", "ZINTERSTORE", "ZUNIONSTORE",
}
if command in multi_key_commands and len(args) >= 1:
return args[0]
if command in second_key_commands and len(args) >= 1:
return args[0]
# 管理命令/无 key 命令:不应提取 key
no_key_commands = {
"CONFIG", "ACL", "AUTH", "INFO", "PING", "FLUSHDB", "FLUSHALL",
"DBSIZE", "BGSAVE", "BGREWRITEAOF", "SAVE", "LASTSAVE",
"SHUTDOWN", "SLAVEOF", "REPLICAOF", "SLOWLOG", "MONITOR",
"DEBUG", "OBJECT", "CLIENT", "CLUSTER", "SENTINEL",
"MODULE", "COMMAND", "MEMORY", "LATENCY", "WAIT",
}
if command in no_key_commands:
return None
# 默认:第一个参数是 key
return args[0]
def _is_redis_command(sql_upper: str) -> bool:
"""判断是否为 Redis 命令行(非空且非注释)。"""
stripped = sql_upper.strip()
return bool(stripped) and not stripped.startswith("#")
def _has_namespace_separator(key: str, separator: str = ":") -> bool:
"""检查 Key 是否包含命名空间分隔符(默认冒号)。"""
return separator in key
def _count_namespaces(key: str, separator: str = ":") -> int:
"""统计 Key 中命名空间层级数。"""
return key.count(separator) + 1
def _extract_ttl_from_args(command: str, args: List[str]) -> Optional[int]:
"""从命令参数中提取 TTL 值(秒)。
检测 EX, PX, EXAT, PXAT 参数。
Returns:
TTL 秒数,如果没有找到则返回 None
"""
if not args:
return None
cmd = command.upper()
# SET ... EX seconds / PX milliseconds / EXAT timestamp / PXAT ms_timestamp
if cmd == "SET" or cmd.startswith("SET"):
for i, arg in enumerate(args):
arg_upper = arg.upper()
if arg_upper == "EX" and i + 1 < len(args):
try:
return int(args[i + 1])
except (ValueError, TypeError):
return None
elif arg_upper == "PX" and i + 1 < len(args):
try:
return int(args[i + 1]) // 1000
except (ValueError, TypeError):
return None
elif arg_upper in ("EXAT", "PXAT") and i + 1 < len(args):
return -1 # 绝对时间戳,视为已设置 TTL
return None
# SETEX key seconds value / PSETEX key milliseconds value
if cmd == "SETEX" and len(args) >= 2:
try:
return int(args[1])
except (ValueError, TypeError):
return None
if cmd == "PSETEX" and len(args) >= 2:
try:
return int(args[1]) // 1000
except (ValueError, TypeError):
return None
# EXPIRE key seconds / PEXPIRE key milliseconds / EXPIREAT key timestamp
if cmd in ("EXPIRE", "EXPIREAT") and len(args) >= 2:
try:
return int(args[1])
except (ValueError, TypeError):
return None
if cmd == "PEXPIRE" and len(args) >= 2:
try:
return int(args[1]) // 1000
except (ValueError, TypeError):
return None
return None
def _is_ttl_set_in_command(sql_clean: str, command: str, args: List[str]) -> bool:
"""检查命令是否设置了 TTL。"""
cmd = command.upper()
# 直接设置 TTL 的命令
ttl_commands = {
"EXPIRE", "PEXPIRE", "EXPIREAT", "PEXPIREAT", "SETEX", "PSETEX"
}
if cmd in ttl_commands:
return True
# SET 命令带 EX/PX/EXAT/PXAT
if cmd in ("SET", "GETSET"):
for i, arg in enumerate(args):
if arg.upper() in ("EX", "PX", "EXAT", "PXAT"):
return True
return False
def _is_write_command(command: str) -> bool:
"""判断是否为写入类命令。"""
write_commands = {
"SET", "GETSET", "MSET", "MSETNX", "SETNX", "SETEX", "PSETEX",
"HSET", "HMSET", "HSETNX", "HDEL", "HINCRBY", "HINCRBYFLOAT",
"LPUSH", "RPUSH", "LPOP", "RPOP", "LSET", "LREM", "LINSERT",
"LTRIM", "LMOVE", "BLMOVE", "RPOPLPUSH", "BRPOPLPUSH",
"SADD", "SREM", "SPOP", "SMOVE", "SDIFFSTORE", "SINTERSTORE", "SUNIONSTORE",
"ZADD", "ZREM", "ZINCRBY", "ZREMRANGEBYRANK", "ZREMRANGEBYSCORE",
"ZREMRANGEBYLEX", "ZDIFFSTORE", "ZINTERSTORE", "ZUNIONSTORE",
"XADD", "XDEL", "XTRIM", "XGROUP", "XSETID",
"DEL", "UNLINK", "FLUSHDB", "FLUSHALL",
"APPEND", "INCR", "DECR", "INCRBY", "DECRBY", "INCRBYFLOAT",
"PFADD", "PFMERGE", "PFCOUNT",
"GEOADD", "GEORADIUS", "GEORADIUSBYMEMBER",
"BITSET", "SETBIT", "BITOP",
"RESTORE", "MIGRATE", "RENAME", "RENAMENX",
}
return command.upper() in write_commands
def _extract_pipeline_commands(sql_clean: str) -> List[str]:
"""从 Pipeline 输入中提取每条命令。
Pipeline 命令通常用换行符分隔。
"""
commands = []
for line in sql_clean.split("\n"):
stripped = line.strip()
if stripped and not stripped.startswith("#"):
commands.append(stripped)
return commands
# ═══════════════════════════════════════════════
# KEY 管理规则 (10条)
# ═══════════════════════════════════════════════
@register_rule(AuditRuleV2(
code="REDIS-KEY-001",
name="Key命名规范",
category=RuleCategory.NAMING,
severity=Severity.WARNING,
db_type="redis",
description="Key 建议使用冒号分隔的命名空间,如 user:1001:profile",
config={"namespace_separator": ":", "min_namespace_levels": 2},
message_template="Key '{key}' 命名不符合规范,建议使用冒号分隔的命名空间(如 app:module:id)",
))
def check_redis_key_001(sql_clean: str, sql_upper: str, report: AuditReportV2, rule: AuditRuleV2) -> None:
"""Key命名规范检查 - 建议使用冒号分隔的命名空间。"""
command, args = _parse_redis_command(sql_clean)
if not command:
return
key = _extract_key_from_command(command, args)
if not key:
return
separator = rule.config.get("namespace_separator", ":")
min_levels = rule.config.get("min_namespace_levels", 2)
if separator not in key or _count_namespaces(key, separator) < min_levels:
report.findings.append(AuditFindingV2(
rule_code=rule.code,
rule_name=rule.name,
level=rule.severity,
message=rule.message_template.format(key=key),
))
@register_rule(AuditRuleV2(
code="REDIS-KEY-002",
name="Key最大长度限制",
category=RuleCategory.NAMING,
severity=Severity.WARNING,
db_type="redis",
description="Key 名称最大长度建议不超过128字节",
config={"max_key_length": 128},
message_template="Key '{key}' 长度为 {length} 字节,超过最大限制 {max} 字节",
))
def check_redis_key_002(sql_clean: str, sql_upper: str, report: AuditReportV2, rule: AuditRuleV2) -> None:
"""Key最大长度限制检查。"""
command, args = _parse_redis_command(sql_clean)
if not command:
return
key = _extract_key_from_command(command, args)
if not key:
return
max_len = rule.config.get("max_key_length", 128)
key_bytes = len(key.encode("utf-8"))
if key_bytes > max_len:
report.findings.append(AuditFindingV2(
rule_code=rule.code,
rule_name=rule.name,
level=rule.severity,
message=rule.message_template.format(key=key, length=key_bytes, max=max_len),
))
@register_rule(AuditRuleV2(
code="REDIS-KEY-003",
name="Key值大小限制",
category=RuleCategory.DDL,
severity=Severity.WARNING,
db_type="redis",
description="String 值建议不超过 10KB,Hash 建议不超过 1000 字段",
config={"max_string_size": 10240, "max_hash_fields": 1000},
message_template="{cmd} 操作可能导致大 Key 问题,String 值建议不超过 {max_str} 字节,Hash 建议不超过 {max_hash} 字段",
))
def check_redis_key_003(sql_clean: str, sql_upper: str, report: AuditReportV2, rule: AuditRuleV2) -> None:
"""Key值大小限制检查 - 针对大 Value 的写入操作给出警告。"""
command, args = _parse_redis_command(sql_clean)
if not command:
return
cmd = command.upper()
# 检测 HMSET/HSET 大量字段(超过阈值的一半即警告)
hash_cmds = {"HSET", "HMSET", "HGETALL"}
if cmd in hash_cmds and len(args) > 20:
max_hash = rule.config.get("max_hash_fields", 1000)
# HMSET key field1 value1 field2 value2 ...
# 字段数 = (args - 1) // 2
field_count = (len(args) - 1) // 2 if cmd in ("HSET", "HMSET") else 0
if field_count > max_hash // 2:
report.findings.append(AuditFindingV2(
rule_code=rule.code,
rule_name=rule.name,
level=rule.severity,
message=f"HMSET/HSET 操作包含约 {field_count} 个字段,接近大 Key 阈值 {max_hash},建议拆分 Hash 结构",
))
return
# 检测大 String 写入
string_cmds = {"SET", "SETEX", "PSETEX", "APPEND", "GETSET"}
if cmd in string_cmds and len(args) >= 2:
max_str = rule.config.get("max_string_size", 10240)
# 找到 value 参数(通常是第2或第3个参数)
value = ""
if cmd in ("SETEX", "PSETEX") and len(args) >= 3:
value = args[2]
elif cmd == "APPEND" and len(args) >= 2:
value = args[1]
elif len(args) >= 2:
value = args[1]
value_bytes = len(value.encode("utf-8")) if value else 0
if value_bytes > max_str:
report.findings.append(AuditFindingV2(
rule_code=rule.code,
rule_name=rule.name,
level=rule.severity,
message=f"SET 操作的 Value 大小为 {value_bytes} 字节,超过建议阈值 {max_str} 字节(10KB)",
))
@register_rule(AuditRuleV2(
code="REDIS-KEY-004",
name="写入Key必须设置TTL",
category=RuleCategory.DDL,
severity=Severity.ERROR,
db_type="redis",
description="写入 Key 必须设置 TTL,不允许永久 Key 存在",
config={"whitelist_keys": []},
message_template="写入 Key '{key}' 未设置 TTL,请添加 EX/PX 参数或单独执行 EXPIRE 命令,禁止创建永久 Key",
))
def check_redis_key_004(sql_clean: str, sql_upper: str, report: AuditReportV2, rule: AuditRuleV2) -> None:
"""写入 Key 必须设置 TTL 检查。"""
command, args = _parse_redis_command(sql_clean)
if not command:
return
cmd = command.upper()
# 只对写入命令检查
if not _is_write_command(cmd):
return
key = _extract_key_from_command(command, args)
if not key:
return
# 检查白名单
whitelist = rule.config.get("whitelist_keys", [])
if any(key.startswith(w) for w in whitelist):
return
# 检查是否已设置 TTL
if _is_ttl_set_in_command(sql_clean, cmd, args):
return
# 排除一些天然带 TTL 的命令和一些特殊的命令
ttl_builtin_cmds = {"SETEX", "PSETEX"}
if cmd in ttl_builtin_cmds:
return
# DEL/UNLINK/FLUSH 等删除命令不需要 TTL
delete_cmds = {"DEL", "UNLINK", "FLUSHDB", "FLUSHALL", "EXPIRE", "PEXPIRE",
"EXPIREAT", "PEXPIREAT", "TTL", "PTTL", "PERSIST"}
if cmd in delete_cmds:
return
# 自增类命令通常是更新已有 Key,不需要设置 TTL
incr_cmds = {"INCR", "DECR", "INCRBY", "DECRBY", "INCRBYFLOAT",
"HINCRBY", "HINCRBYFLOAT", "ZINCRBY"}
if cmd in incr_cmds:
return
report.findings.append(AuditFindingV2(
rule_code=rule.code,
rule_name=rule.name,
level=rule.severity,
message=rule.message_template.format(key=key),
))
@register_rule(AuditRuleV2(
code="REDIS-KEY-005",
name="默认TTL建议",
category=RuleCategory.DDL,
severity=Severity.INFO,
db_type="redis",
description="建议设置合理的默认 TTL,如 3600 秒(1小时)",
config={"recommended_default_ttl": 3600, "min_recommended_ttl": 60},
message_template="建议为 Key '{key}' 设置合理的 TTL,默认推荐 {ttl} 秒(1小时),避免数据永久驻留",
))
def check_redis_key_005(sql_clean: str, sql_upper: str, report: AuditReportV2, rule: AuditRuleV2) -> None:
"""默认 TTL 建议 - 对未设置 TTL 或 TTL 过短的命令给出提示。"""
command, args = _parse_redis_command(sql_clean)
if not command:
return
cmd = command.upper()
# 只检查写入命令
if not _is_write_command(cmd):
return
key = _extract_key_from_command(command, args)
if not key:
return
ttl = _extract_ttl_from_args(cmd, args)
if ttl is None:
return # 已由 REDIS-KEY-004 检查
min_ttl = rule.config.get("min_recommended_ttl", 60)
if ttl > 0 and ttl < min_ttl:
default_ttl = rule.config.get("recommended_default_ttl", 3600)
report.findings.append(AuditFindingV2(
rule_code=rule.code,
rule_name=rule.name,
level=rule.severity,
message=f"Key '{key}' 的 TTL 设置为 {ttl} 秒,过短可能导致频繁缓存失效,建议至少 {min_ttl} 秒或默认 {default_ttl} 秒",
))
@register_rule(AuditRuleV2(
code="REDIS-KEY-006",
name="最大TTL限制",
category=RuleCategory.DDL,
severity=Severity.WARNING,
db_type="redis",
description="Key 的 TTL 建议不超过 30 天(2592000 秒)",
config={"max_ttl_seconds": 2592000},
message_template="Key '{key}' 的 TTL {ttl} 秒超过最大限制 {max} 秒({days} 天),建议缩短过期时间",
))
def check_redis_key_006(sql_clean: str, sql_upper: str, report: AuditReportV2, rule: AuditRuleV2) -> None:
"""最大 TTL 限制检查。"""
command, args = _parse_redis_command(sql_clean)
if not command:
return
cmd = command.upper()
ttl = _extract_ttl_from_args(cmd, args)
if ttl is None or ttl <= 0:
return
max_ttl = rule.config.get("max_ttl_seconds", 2592000)
if ttl > max_ttl:
key = _extract_key_from_command(command, args) or "unknown"
days = max_ttl // 86400
report.findings.append(AuditFindingV2(
rule_code=rule.code,
rule_name=rule.name,
level=rule.severity,
message=rule.message_template.format(key=key, ttl=ttl, max=max_ttl, days=days),
))
@register_rule(AuditRuleV2(
code="REDIS-KEY-007",
name="禁止使用KEYS命令",
category=RuleCategory.PERFORMANCE,
severity=Severity.ERROR,
db_type="redis",
description="KEYS 命令会扫描全库,阻塞服务器,必须使用 SCAN 替代",
message_template="禁止使用 KEYS 命令('{cmd}'),会导致全库扫描阻塞服务器,请使用 SCAN 替代",
))
def check_redis_key_007(sql_clean: str, sql_upper: str, report: AuditReportV2, rule: AuditRuleV2) -> None:
"""禁止使用 KEYS 命令。"""
command, args = _parse_redis_command(sql_clean)
if not command:
return
if command.upper() == "KEYS":
report.findings.append(AuditFindingV2(
rule_code=rule.code,
rule_name=rule.name,
level=rule.severity,
message=rule.message_template.format(cmd=sql_clean.strip()),
))
@register_rule(AuditRuleV2(
code="REDIS-KEY-008",
name="必须指定命名空间前缀",
category=RuleCategory.NAMING,
severity=Severity.WARNING,
db_type="redis",
description="Key 必须以指定的命名空间前缀开头,如 app:service:key",
config={"required_prefixes": ["app:", "svc:", "cache:", "session:", "mq:"]},
message_template="Key '{key}' 缺少命名空间前缀,Key 必须以 {prefixes} 之一开头",
))
def check_redis_key_008(sql_clean: str, sql_upper: str, report: AuditReportV2, rule: AuditRuleV2) -> None:
"""必须指定命名空间前缀检查。"""
command, args = _parse_redis_command(sql_clean)
if not command:
return
key = _extract_key_from_command(command, args)
if not key:
return
prefixes = rule.config.get("required_prefixes", ["app:", "svc:", "cache:", "session:", "mq:"])
if not any(key.startswith(p) for p in prefixes):
report.findings.append(AuditFindingV2(
rule_code=rule.code,
rule_name=rule.name,
level=rule.severity,
message=rule.message_template.format(key=key, prefixes=", ".join(prefixes)),
))
@register_rule(AuditRuleV2(
code="REDIS-KEY-009",
name="禁止批量删除Key模式",
category=RuleCategory.SECURITY,
severity=Severity.ERROR,
db_type="redis",
description="禁止使用 DEL key:* 等通配符批量删除 Key 的模式",
config={},
message_template="禁止使用通配符批量删除 Key 的模式 '{cmd}',请使用 SCAN + DEL 循环逐个删除",
))
def check_redis_key_009(sql_clean: str, sql_upper: str, report: AuditReportV2, rule: AuditRuleV2) -> None:
"""禁止批量删除 Key 模式检查。
检测使用通配符配合 DEL 的模式,如:
- DEL user:*
- EVAL "... KEYS[1] ..." 0 prefix:*
"""
command, args = _parse_redis_command(sql_clean)
if not command:
return
cmd = command.upper()
# 检查 DEL/UNLINK 命令参数中包含通配符 *
if cmd in ("DEL", "UNLINK") and args:
for arg in args:
if "*" in arg or "?" in arg:
report.findings.append(AuditFindingV2(
rule_code=rule.code,
rule_name=rule.name,
level=rule.severity,
message=rule.message_template.format(cmd=sql_clean.strip()),
))
return
# 检查 EVAL/EVALSHA 脚本中的批量删除模式
if cmd in ("EVAL", "EVALSHA") and args:
script_body = args[0].upper()
if "DEL" in script_body and ("KEYS" in script_body or "ARGV" in script_body):
report.findings.append(AuditFindingV2(
rule_code=rule.code,
rule_name=rule.name,
level=rule.severity,
message=f"Lua 脚本 '{cmd}' 中可能包含批量删除操作,请确保使用 SCAN 逐个删除",
))
@register_rule(AuditRuleV2(
code="REDIS-KEY-010",
name="推荐高效编码",
category=RuleCategory.DDL,
severity=Severity.INFO,
db_type="redis",
description="建议开启 ziplist/intset 等高效编码以节省内存",
config={"ziplist_max_entries": 512, "ziplist_max_value": 64},
message_template="建议配置 ziplist/intset 高效编码参数(hash-max-ziplist-entries/list-max-ziplist-entries/set-max-intset-entries),节省内存",
))
def check_redis_key_010(sql_clean: str, sql_upper: str, report: AuditReportV2, rule: AuditRuleV2) -> None:
"""推荐高效编码检查 - 检测 CONFIG SET 命令中关于编码的配置。
当用户执行 CONFIG SET 修改编码相关参数时进行检查和提示。
"""
command, args = _parse_redis_command(sql_clean)
if not command:
return
cmd = command.upper()
if cmd == "CONFIG" and len(args) >= 2:
subcmd = args[0].upper()
param = args[1].upper()
if subcmd == "SET" and "ziplist" in param.lower():
try:
value = int(args[2]) if len(args) >= 3 else 0
max_entries = rule.config.get("ziplist_max_entries", 512)
if value > max_entries:
report.findings.append(AuditFindingV2(
rule_code=rule.code,
rule_name=rule.name,
level=Severity.INFO,
message=f"ziplist 条目数设置为 {value},超过建议值 {max_entries},过大可能导致性能下降",
))
except (ValueError, TypeError, IndexError):
pass
# 对于 Hash 操作,如果字段数较少但使用了大配置,给出提示
if cmd in ("HSET", "HMSET") and len(args) >= 4:
field_count = (len(args) - 1) // 2
max_entries = 512
if field_count <= max_entries:
report.findings.append(AuditFindingV2(
rule_code=rule.code,
rule_name=rule.name,
level=Severity.INFO,
message=f"Hash 字段数为 {field_count},建议配置 hash-max-ziplist-entries >= {max_entries} 以启用 ziplist 编码节省内存",
))
# ═══════════════════════════════════════════════
# 命令安全规则 (12条)
# ═══════════════════════════════════════════════
@register_rule(AuditRuleV2(
code="REDIS-CMD-001",
name="禁止FLUSHDB",
category=RuleCategory.SECURITY,
severity=Severity.ERROR,
db_type="redis",
description="禁止执行 FLUSHDB 命令,将清空当前数据库所有数据",
message_template="禁止执行 FLUSHDB,该命令将清空当前数据库所有数据,如需清理请使用 SCAN + DEL 逐个删除",
))
def check_redis_cmd_001(sql_clean: str, sql_upper: str, report: AuditReportV2, rule: AuditRuleV2) -> None:
"""禁止 FLUSHDB。"""
command, args = _parse_redis_command(sql_clean)
if command.upper() == "FLUSHDB":
report.findings.append(AuditFindingV2(
rule_code=rule.code,
rule_name=rule.name,
level=rule.severity,
message=rule.message_template,
))
@register_rule(AuditRuleV2(
code="REDIS-CMD-002",
name="禁止FLUSHALL",
category=RuleCategory.SECURITY,
severity=Severity.ERROR,
db_type="redis",
description="禁止执行 FLUSHALL 命令,将清空所有数据库数据",
message_template="禁止执行 FLUSHALL,该命令将清空所有数据库数据,属于高危操作",
))
def check_redis_cmd_002(sql_clean: str, sql_upper: str, report: AuditReportV2, rule: AuditRuleV2) -> None:
"""禁止 FLUSHALL。"""
command, args = _parse_redis_command(sql_clean)
if command.upper() == "FLUSHALL":
report.findings.append(AuditFindingV2(
rule_code=rule.code,
rule_name=rule.name,
level=rule.severity,
message=rule.message_template,
))
@register_rule(AuditRuleV2(
code="REDIS-CMD-003",
name="禁止SHUTDOWN",
category=RuleCategory.SECURITY,
severity=Severity.ERROR,
db_type="redis",
description="禁止执行 SHUTDOWN 命令,将关闭 Redis 服务器",
message_template="禁止执行 SHUTDOWN,该命令将关闭 Redis 服务器,如需维护请联系 DBA",
))
def check_redis_cmd_003(sql_clean: str, sql_upper: str, report: AuditReportV2, rule: AuditRuleV2) -> None:
"""禁止 SHUTDOWN。"""
command, args = _parse_redis_command(sql_clean)
if command.upper() == "SHUTDOWN":
report.findings.append(AuditFindingV2(
rule_code=rule.code,
rule_name=rule.name,
level=rule.severity,
message=rule.message_template,
))
@register_rule(AuditRuleV2(
code="REDIS-CMD-004",
name="禁止CONFIG SET",
category=RuleCategory.SECURITY,
severity=Severity.ERROR,
db_type="redis",
description="禁止执行 CONFIG SET 命令修改 Redis 配置",
message_template="禁止执行 CONFIG SET 修改配置,参数调整需通过运维流程审批后由 DBA 执行",
))
def check_redis_cmd_004(sql_clean: str, sql_upper: str, report: AuditReportV2, rule: AuditRuleV2) -> None:
"""禁止 CONFIG SET。"""
command, args = _parse_redis_command(sql_clean)
if command.upper() == "CONFIG" and len(args) >= 1:
if args[0].upper() == "SET":
report.findings.append(AuditFindingV2(
rule_code=rule.code,
rule_name=rule.name,
level=rule.severity,
message=rule.message_template,
))
@register_rule(AuditRuleV2(
code="REDIS-CMD-005",
name="禁止DEBUG命令",
category=RuleCategory.SECURITY,
severity=Severity.ERROR,
db_type="redis",
description="禁止执行 DEBUG 命令(含 DEBUG SEGFAULT 等调试命令)",
message_template="禁止执行 DEBUG 命令('{cmd}'),调试命令可能导致服务崩溃或性能问题",
))
def check_redis_cmd_005(sql_clean: str, sql_upper: str, report: AuditReportV2, rule: AuditRuleV2) -> None:
"""禁止 DEBUG 命令。"""
command, args = _parse_redis_command(sql_clean)
if command.upper() == "DEBUG":
report.findings.append(AuditFindingV2(
rule_code=rule.code,
rule_name=rule.name,
level=rule.severity,
message=rule.message_template.format(cmd=sql_clean.strip()),
))
@register_rule(AuditRuleV2(
code="REDIS-CMD-006",
name="禁止REPLICAOF/SLAVEOF",
category=RuleCategory.SECURITY,
severity=Severity.ERROR,
db_type="redis",
description="禁止执行 REPLICAOF/SLAVEOF 命令修改主从配置",
message_template="禁止执行 {cmd} 修改主从复制配置,主从变更需通过运维流程审批",
))
def check_redis_cmd_006(sql_clean: str, sql_upper: str, report: AuditReportV2, rule: AuditRuleV2) -> None:
"""禁止 REPLICAOF/SLAVEOF。"""
command, args = _parse_redis_command(sql_clean)
cmd = command.upper()
if cmd in ("REPLICAOF", "SLAVEOF"):
report.findings.append(AuditFindingV2(
rule_code=rule.code,
rule_name=rule.name,
level=rule.severity,
message=rule.message_template.format(cmd=cmd),
))
@register_rule(AuditRuleV2(
code="REDIS-CMD-007",
name="禁止MIGRATE",
category=RuleCategory.SECURITY,
severity=Severity.ERROR,
db_type="redis",
description="禁止执行 MIGRATE 命令进行数据迁移",
message_template="禁止执行 MIGRATE 数据迁移命令,数据迁移需通过 DBA 审批和协助",
))
def check_redis_cmd_007(sql_clean: str, sql_upper: str, report: AuditReportV2, rule: AuditRuleV2) -> None:
"""禁止 MIGRATE。"""
command, args = _parse_redis_command(sql_clean)
if command.upper() == "MIGRATE":
report.findings.append(AuditFindingV2(
rule_code=rule.code,
rule_name=rule.name,
level=rule.severity,
message=rule.message_template,
))
@register_rule(AuditRuleV2(
code="REDIS-CMD-008",
name="禁止RESTORE",
category=RuleCategory.SECURITY,
severity=Severity.ERROR,
db_type="redis",
description="禁止执行 RESTORE 命令恢复序列化数据",
message_template="禁止执行 RESTORE 恢复数据命令,数据恢复需通过 DBA 审批和协助",
))
def check_redis_cmd_008(sql_clean: str, sql_upper: str, report: AuditReportV2, rule: AuditRuleV2) -> None:
"""禁止 RESTORE。"""
command, args = _parse_redis_command(sql_clean)
if command.upper() == "RESTORE":
report.findings.append(AuditFindingV2(
rule_code=rule.code,
rule_name=rule.name,
level=rule.severity,
message=rule.message_template,
))
@register_rule(AuditRuleV2(
code="REDIS-CMD-009",
name="禁止EVAL/EVALSHA",
category=RuleCategory.SECURITY,
severity=Severity.ERROR,
db_type="redis",
description="禁止执行 EVAL/EVALSHA 运行 Lua 脚本(未授权脚本风险)",
config={"allowed_scripts": []},
message_template="禁止执行 {cmd} 运行 Lua 脚本,未授权脚本可能导致性能问题或安全风险,请使用 Function 或提交审批",
))
def check_redis_cmd_009(sql_clean: str, sql_upper: str, report: AuditReportV2, rule: AuditRuleV2) -> None:
"""禁止 EVAL/EVALSHA。"""
command, args = _parse_redis_command(sql_clean)
cmd = command.upper()
if cmd in ("EVAL", "EVALSHA"):
report.findings.append(AuditFindingV2(
rule_code=rule.code,
rule_name=rule.name,
level=rule.severity,
message=rule.message_template.format(cmd=cmd),
))
@register_rule(AuditRuleV2(
code="REDIS-CMD-010",
name="禁止MODULE LOAD",
category=RuleCategory.SECURITY,
severity=Severity.ERROR,
db_type="redis",
description="禁止执行 MODULE LOAD 加载外部模块(安全风险)",
message_template="禁止执行 MODULE LOAD 加载外部模块,模块加载需通过安全审批和代码审计",
))
def check_redis_cmd_010(sql_clean: str, sql_upper: str, report: AuditReportV2, rule: AuditRuleV2) -> None:
"""禁止 MODULE LOAD。"""
command, args = _parse_redis_command(sql_clean)
cmd = command.upper()
if cmd == "MODULE" and len(args) >= 1 and args[0].upper() == "LOAD":
report.findings.append(AuditFindingV2(
rule_code=rule.code,
rule_name=rule.name,
level=rule.severity,
message=rule.message_template,
))
@register_rule(AuditRuleV2(
code="REDIS-CMD-011",
name="禁止ACL SETUSER",
category=RuleCategory.SECURITY,
severity=Severity.ERROR,
db_type="redis",
description="禁止执行 ACL SETUSER 修改用户权限",
message_template="禁止执行 ACL SETUSER 修改用户权限,账户管理需通过运维流程",
))
def check_redis_cmd_011(sql_clean: str, sql_upper: str, report: AuditReportV2, rule: AuditRuleV2) -> None:
"""禁止 ACL SETUSER。"""
command, args = _parse_redis_command(sql_clean)
cmd = command.upper()
if cmd == "ACL" and len(args) >= 1:
if args[0].upper() in ("SETUSER", "DELUSER", "LOAD", "SAVE", "GENPASS"):
report.findings.append(AuditFindingV2(
rule_code=rule.code,
rule_name=rule.name,
level=rule.severity,
message=rule.message_template,
))
@register_rule(AuditRuleV2(
code="REDIS-CMD-012",
name="限制MONITOR命令",
category=RuleCategory.PERFORMANCE,
severity=Severity.WARNING,
db_type="redis",
description="MONITOR 命令会导致严重的性能影响,生产环境限制使用",
message_template="MONITOR 命令会导致严重的性能影响(所有命令输出到客户端),生产环境请限制使用",
))
def check_redis_cmd_012(sql_clean: str, sql_upper: str, report: AuditReportV2, rule: AuditRuleV2) -> None:
"""限制 MONITOR 命令。"""
command, args = _parse_redis_command(sql_clean)
if command.upper() == "MONITOR":
report.findings.append(AuditFindingV2(
rule_code=rule.code,
rule_name=rule.name,
level=rule.severity,
message=rule.message_template,
))
# ═══════════════════════════════════════════════
# 数据结构规则 (8条)
# ═══════════════════════════════════════════════
@register_rule(AuditRuleV2(
code="REDIS-DS-001",
name="String值最大大小限制",
category=RuleCategory.DDL,
severity=Severity.WARNING,
db_type="redis",
description="String 类型值大小建议不超过 10MB",
config={"max_string_size_mb": 10, "max_string_size_bytes": 10485760},
message_template="String 值大小约为 {size} 字节,超过建议阈值 {max} 字节({max_mb}MB),建议使用 Hash/List 拆分存储",
))
def check_redis_ds_001(sql_clean: str, sql_upper: str, report: AuditReportV2, rule: AuditRuleV2) -> None:
"""String 值最大大小限制检查。"""
command, args = _parse_redis_command(sql_clean)
if not command:
return
cmd = command.upper()
string_write_cmds = {"SET", "SETEX", "PSETEX", "GETSET", "APPEND"}
if cmd not in string_write_cmds or len(args) < 2:
return
value = ""
if cmd in ("SETEX", "PSETEX") and len(args) >= 3:
value = args[2]
elif cmd in ("SET", "GETSET", "APPEND") and len(args) >= 2:
value = args[1]
max_bytes = rule.config.get("max_string_size_bytes", 10485760)
value_bytes = len(value.encode("utf-8")) if value else 0
if value_bytes > max_bytes:
max_mb = rule.config.get("max_string_size_mb", 10)
report.findings.append(AuditFindingV2(
rule_code=rule.code,
rule_name=rule.name,
level=rule.severity,
message=rule.message_template.format(size=value_bytes, max=max_bytes, max_mb=max_mb),
))
@register_rule(AuditRuleV2(
code="REDIS-DS-002",
name="Hash最大字段数限制",
category=RuleCategory.DDL,
severity=Severity.WARNING,
db_type="redis",
description="Hash 类型字段数建议不超过 10000",
config={"max_hash_fields": 10000},
message_template="Hash 字段数 {fields} 超过建议阈值 {max},请拆分 Hash 或使用多个 Key 存储",
))
def check_redis_ds_002(sql_clean: str, sql_upper: str, report: AuditReportV2, rule: AuditRuleV2) -> None:
"""Hash 最大字段数限制检查。"""
command, args = _parse_redis_command(sql_clean)
if not command:
return
cmd = command.upper()
if cmd in ("HMSET", "HSET") and len(args) >= 3:
field_count = (len(args) - 1) // 2
max_fields = rule.config.get("max_hash_fields", 10000)
if field_count > max_fields:
report.findings.append(AuditFindingV2(
rule_code=rule.code,
rule_name=rule.name,
level=rule.severity,
message=rule.message_template.format(fields=field_count, max=max_fields),
))
# HGETALL 读取大 Hash 也警告
if cmd == "HGETALL" and len(args) >= 1:
max_fields = rule.config.get("max_hash_fields", 10000)
report.findings.append(AuditFindingV2(
rule_code=rule.code,
rule_name=rule.name,
level=Severity.INFO,
message=f"HGETALL 读取 Hash '{args[0]}' 可能返回大量字段,建议使用 HSCAN 分批读取,避免阻塞",
))
@register_rule(AuditRuleV2(
code="REDIS-DS-003",
name="List最大长度限制",
category=RuleCategory.DDL,
severity=Severity.WARNING,
db_type="redis",
description="List 类型长度建议不超过 100000",
config={"max_list_length": 100000},
message_template="List 长度可能超过建议阈值 {max},请使用 LTRIM 控制长度或使用 Stream 替代",
))
def check_redis_ds_003(sql_clean: str, sql_upper: str, report: AuditReportV2, rule: AuditRuleV2) -> None:
"""List 最大长度限制检查。
检测 List 写入命令,当未带 LTRIM/XTRIM 等长度控制时警告。
"""
command, args = _parse_redis_command(sql_clean)
if not command:
return
cmd = command.upper()
list_push_cmds = {"LPUSH", "RPUSH", "LPUSHX", "RPUSHX"}
if cmd in list_push_cmds and len(args) >= 2:
max_len = rule.config.get("max_list_length", 100000)
# 大量元素一次性 LPUSH/RPUSH
element_count = len(args) - 1 # 减去 key
if element_count > 100:
report.findings.append(AuditFindingV2(
rule_code=rule.code,
rule_name=rule.name,
level=Severity.INFO,
message=f"一次性 {cmd} 插入 {element_count} 个元素,List 长度可能增长过快,建议使用 LTRIM 控制长度不超过 {max_len}",
))
# LRANGE 大跨度读取
if cmd == "LRANGE" and len(args) >= 3:
try:
start = int(args[1])
end = int(args[2])
range_size = abs(end - start) + 1 if end >= 0 else abs(start) + abs(end)
max_len = rule.config.get("max_list_length", 100000)
if range_size > max_len // 10:
report.findings.append(AuditFindingV2(
rule_code=rule.code,
rule_name=rule.name,
level=rule.severity,
message=f"LRANGE 范围 {start} ~ {end} 涉及约 {range_size} 个元素,范围过大可能导致阻塞,建议分页读取",
))
except (ValueError, TypeError):
pass
@register_rule(AuditRuleV2(
code="REDIS-DS-004",
name="Set最大成员数限制",
category=RuleCategory.DDL,
severity=Severity.WARNING,
db_type="redis",
description="Set 类型成员数建议不超过 100000",
config={"max_set_members": 100000},
message_template="Set 成员数 {members} 超过建议阈值 {max},请拆分 Set 或使用 BloomFilter 替代",
))
def check_redis_ds_004(sql_clean: str, sql_upper: str, report: AuditReportV2, rule: AuditRuleV2) -> None:
"""Set 最大成员数限制检查。"""
command, args = _parse_redis_command(sql_clean)
if not command:
return
cmd = command.upper()
# SADD 大量成员
if cmd == "SADD" and len(args) > 100:
member_count = len(args) - 1
max_members = rule.config.get("max_set_members", 100000)
if member_count > max_members // 10:
report.findings.append(AuditFindingV2(
rule_code=rule.code,
rule_name=rule.name,
level=Severity.INFO,
message=f"SADD 一次性添加 {member_count} 个成员,Set 过大(>{max_members})会影响性能,建议拆分",
))
# SMEMBERS 全量读取大 Set 警告
if cmd == "SMEMBERS" and len(args) >= 1:
max_members = rule.config.get("max_set_members", 100000)
report.findings.append(AuditFindingV2(
rule_code=rule.code,
rule_name=rule.name,
level=Severity.INFO,
message=f"SMEMBERS 全量读取 Set '{args[0]}' 可能返回大量数据,建议使用 SSCAN 分批扫描(成员数建议不超过 {max_members})",
))
@register_rule(AuditRuleV2(
code="REDIS-DS-005",
name="ZSet最大成员数限制",
category=RuleCategory.DDL,
severity=Severity.WARNING,
db_type="redis",
description="ZSet 类型成员数建议不超过 100000",
config={"max_zset_members": 100000},
message_template="ZSet 成员数超过建议阈值 {max},请拆分 ZSet 或优化数据模型",
))
def check_redis_ds_005(sql_clean: str, sql_upper: str, report: AuditReportV2, rule: AuditRuleV2) -> None:
"""ZSet 最大成员数限制检查。"""
command, args = _parse_redis_command(sql_clean)
if not command:
return
cmd = command.upper()
# ZADD 大量成员
if cmd == "ZADD" and len(args) > 200:
member_count = (len(args) - 1) // 2
max_members = rule.config.get("max_zset_members", 100000)
if member_count > max_members // 10:
report.findings.append(AuditFindingV2(
rule_code=rule.code,
rule_name=rule.name,
level=Severity.INFO,
message=f"ZADD 一次性添加 {member_count} 个成员,ZSet 过大(>{max_members})会影响排序性能,建议拆分",
))
# ZRANGE/ZREVRANGE 大跨度读取
if cmd in ("ZRANGE", "ZREVRANGE") and len(args) >= 3:
try:
start = int(args[1])
end = int(args[2])
if end < 0 or (end - start) > 10000:
max_members = rule.config.get("max_zset_members", 100000)
report.findings.append(AuditFindingV2(
rule_code=rule.code,
rule_name=rule.name,
level=Severity.INFO,
message=f"{cmd} 大范围读取({start}~{end})可能阻塞,建议分页(每次不超过 1000)或限制 ZSet 大小(<{max_members})",
))
except (ValueError, TypeError):
pass
@register_rule(AuditRuleV2(
code="REDIS-DS-006",
name="Stream最大长度限制",
category=RuleCategory.DDL,
severity=Severity.WARNING,
db_type="redis",
description="Stream 类型必须限制最大长度(使用 MAXLEN),建议不超过 100000",
config={"max_stream_length": 100000},
message_template="Stream 写入命令 '{cmd}' 未使用 MAXLEN 限制长度,请添加 MAXLEN ~ {max} 防止内存无限增长",
))
def check_redis_ds_006(sql_clean: str, sql_upper: str, report: AuditReportV2, rule: AuditRuleV2) -> None:
"""Stream 最大长度限制检查。"""
command, args = _parse_redis_command(sql_clean)
if not command:
return
cmd = command.upper()
if cmd == "XADD" and len(args) >= 2:
has_maxlen = any(arg.upper() == "MAXLEN" for arg in args)
if not has_maxlen:
max_len = rule.config.get("max_stream_length", 100000)
report.findings.append(AuditFindingV2(
rule_code=rule.code,
rule_name=rule.name,
level=rule.severity,
message=rule.message_template.format(cmd=sql_clean.strip(), max=max_len),
))
# XTRIM 命令也需要检查
if cmd == "XTRIM" and len(args) >= 1:
has_maxlen = any(arg.upper() == "MAXLEN" for arg in args)
if not has_maxlen:
report.findings.append(AuditFindingV2(
rule_code=rule.code,
rule_name=rule.name,
level=Severity.INFO,
message="XTRIM 命令未使用 MAXLEN 参数,请确保使用正确的修剪策略",
))
@register_rule(AuditRuleV2(
code="REDIS-DS-007",
name="Bitmap最大大小限制",
category=RuleCategory.DDL,
severity=Severity.WARNING,
db_type="redis",
description="Bitmap 类型大小建议不超过 512MB(offset < 2^32)",
config={"max_bitmap_size_mb": 512, "max_bitmap_offset": 4294967296},
message_template="Bitmap offset {offset} 超过建议阈值 {max_offset}({max_mb}MB),Bitmap 过大消耗大量内存",
))
def check_redis_ds_007(sql_clean: str, sql_upper: str, report: AuditReportV2, rule: AuditRuleV2) -> None:
"""Bitmap 最大大小限制检查。"""
command, args = _parse_redis_command(sql_clean)
if not command:
return
cmd = command.upper()
bitmap_cmds = {"SETBIT", "GETBIT", "BITPOS", "BITCOUNT", "BITOP"}
if cmd not in bitmap_cmds:
return
max_offset = rule.config.get("max_bitmap_offset", 4294967296)
max_mb = rule.config.get("max_bitmap_size_mb", 512)
# SETBIT key offset value
if cmd == "SETBIT" and len(args) >= 2:
try:
offset = int(args[1])
if offset > max_offset:
report.findings.append(AuditFindingV2(
rule_code=rule.code,
rule_name=rule.name,
level=rule.severity,
message=rule.message_template.format(offset=offset, max_offset=max_offset, max_mb=max_mb),
))
except (ValueError, TypeError):
pass
# BITOP 操作可能产生大 Bitmap
if cmd == "BITOP" and len(args) >= 3:
report.findings.append(AuditFindingV2(
rule_code=rule.code,
rule_name=rule.name,
level=Severity.INFO,
message=f"BITOP 操作涉及 {len(args) - 2} 个 Bitmap,请确保结果 Bitmap 大小不超过 {max_mb}MB",
))
@register_rule(AuditRuleV2(
code="REDIS-DS-008",
name="Geo最大成员数限制",
category=RuleCategory.DDL,
severity=Severity.WARNING,
db_type="redis",
description="Geo 类型成员数建议不超过 10000000",
config={"max_geo_members": 10000000},
message_template="Geo 集合成员数 {members} 超过建议阈值 {max},大集合 GEOSEARCH/GEORADIUS 性能会显著下降",
))
def check_redis_ds_008(sql_clean: str, sql_upper: str, report: AuditReportV2, rule: AuditRuleV2) -> None:
"""Geo 最大成员数限制检查。"""
command, args = _parse_redis_command(sql_clean)
if not command:
return
cmd = command.upper()
# GEOADD 大量成员
if cmd == "GEOADD" and len(args) > 300:
member_count = (len(args) - 1) // 3
max_members = rule.config.get("max_geo_members", 10000000)
if member_count > max_members // 100:
report.findings.append(AuditFindingV2(
rule_code=rule.code,
rule_name=rule.name,
level=Severity.INFO,
message=f"GEOADD 一次性添加 {member_count} 个地理坐标,Geo 集合过大(>{max_members})会显著降低查询性能,建议按区域分集合存储",
))
# GEORADIUS/GEOSEARCH 大范围查询
if cmd in ("GEORADIUS", "GEORADIUSBYMEMBER", "GEOSEARCH"):
if len(args) >= 4:
report.findings.append(AuditFindingV2(
rule_code=rule.code,
rule_name=rule.name,
level=Severity.INFO,
message=f"{cmd} 地理范围查询可能返回大量结果,建议使用 COUNT 参数限制返回数量",
))
# ═══════════════════════════════════════════════
# 性能规则 (8条)
# ═══════════════════════════════════════════════
@register_rule(AuditRuleV2(
code="REDIS-PERF-001",
name="禁止阻塞命令",
category=RuleCategory.PERFORMANCE,
severity=Severity.ERROR,
db_type="redis",
description="禁止使用 BLPOP/BRPOP/BRPOPLPUSH/BLMOVE 等阻塞命令",
message_template="禁止执行阻塞命令 '{cmd}',会导致客户端连接长时间阻塞,请使用 LPOP/RPOP/LMOVE 非阻塞版本",
))
def check_redis_perf_001(sql_clean: str, sql_upper: str, report: AuditReportV2, rule: AuditRuleV2) -> None:
"""禁止阻塞命令检查。"""
command, args = _parse_redis_command(sql_clean)
if not command:
return
cmd = command.upper()
blocking_cmds = {"BLPOP", "BRPOP", "BRPOPLPUSH", "BLMOVE", "BZPOPMIN", "BZPOPMAX"}
if cmd in blocking_cmds:
report.findings.append(AuditFindingV2(
rule_code=rule.code,
rule_name=rule.name,
level=rule.severity,
message=rule.message_template.format(cmd=cmd),
))
@register_rule(AuditRuleV2(
code="REDIS-PERF-002",
name="SORT命令必须带LIMIT",
category=RuleCategory.PERFORMANCE,
severity=Severity.WARNING,
db_type="redis",
description="SORT 命令必须带 LIMIT 参数,防止返回过多数据",
message_template="SORT 命令未带 LIMIT 参数,可能返回大量数据导致阻塞,请添加 LIMIT offset count",
))
def check_redis_perf_002(sql_clean: str, sql_upper: str, report: AuditReportV2, rule: AuditRuleV2) -> None:
"""SORT 命令必须带 LIMIT 检查。"""
command, args = _parse_redis_command(sql_clean)
if not command:
return
cmd = command.upper()
if cmd == "SORT" and len(args) >= 1:
has_limit = any(arg.upper() == "LIMIT" for arg in args)
if not has_limit:
report.findings.append(AuditFindingV2(
rule_code=rule.code,
rule_name=rule.name,
level=rule.severity,
message=rule.message_template,
))
@register_rule(AuditRuleV2(
code="REDIS-PERF-003",
name="Pipeline批量限制",
category=RuleCategory.PERFORMANCE,
severity=Severity.WARNING,
db_type="redis",
description="Pipeline 批量命令数建议不超过 1000 条",
config={"max_pipeline_commands": 1000},
message_template="Pipeline 包含 {count} 条命令,超过建议阈值 {max} 条,请分批执行(建议每批不超过 {max} 条)",
))
def check_redis_perf_003(sql_clean: str, sql_upper: str, report: AuditReportV2, rule: AuditRuleV2) -> None:
"""Pipeline 批量限制检查。
对多行输入的命令进行计数。
"""
commands = _extract_pipeline_commands(sql_clean)
max_cmds = rule.config.get("max_pipeline_commands", 1000)
if len(commands) > max_cmds:
report.findings.append(AuditFindingV2(
rule_code=rule.code,
rule_name=rule.name,
level=rule.severity,
message=rule.message_template.format(count=len(commands), max=max_cmds),
))
@register_rule(AuditRuleV2(
code="REDIS-PERF-004",
name="大Key内存使用检测",
category=RuleCategory.PERFORMANCE,
severity=Severity.WARNING,
db_type="redis",
description="检测可能产生大 Key 的操作,建议定期执行 MEMORY USAGE 检查",
config={"big_key_threshold_bytes": 10485760}, # 10MB
message_template="{cmd} 操作 Key '{key}' 可能产生大 Key(>{threshold}MB),建议执行 MEMORY USAGE {key} 检查",
))
def check_redis_perf_004(sql_clean: str, sql_upper: str, report: AuditReportV2, rule: AuditRuleV2) -> None:
"""大 Key 内存使用检测。
对可能产生大 Key 的操作进行静态检测和警告。
"""
command, args = _parse_redis_command(sql_clean)
if not command:
return
cmd = command.upper()
threshold = rule.config.get("big_key_threshold_bytes", 10485760)
threshold_mb = threshold / 1048576
# 检测大 Value 写入
big_value_cmds = {
"SET": (1, "value"), "SETEX": (2, "value"), "PSETEX": (2, "value"),
"HSET": (slice(2, None, 2), "hash fields"),
"HMSET": (slice(2, None, 2), "hash fields"),
"SADD": (slice(1, None), "set members"),
"ZADD": (slice(2, None, 2), "zset members"),
"LPUSH": (slice(1, None), "list elements"),
"RPUSH": (slice(1, None), "list elements"),
"XADD": (slice(2, None, 2), "stream fields"),
}
if cmd in big_value_cmds and len(args) >= 1:
key = args[0]
index_info, desc = big_value_cmds[cmd]
# 检查 value 总大小
total_size = 0
if isinstance(index_info, slice):
values = args[index_info]
for v in values:
total_size += len(v.encode("utf-8"))
elif isinstance(index_info, int) and len(args) > index_info:
total_size = len(args[index_info].encode("utf-8"))
if total_size > threshold:
report.findings.append(AuditFindingV2(
rule_code=rule.code,
rule_name=rule.name,
level=rule.severity,
message=rule.message_template.format(cmd=cmd, key=key, threshold=threshold_mb),
))
# 检测全量读取大 Key 的命令
full_read_cmds = {"HGETALL", "SMEMBERS", "ZRANGE", "ZREVRANGE", "LRANGE", "XREAD", "XREVRANGE"}
if cmd in full_read_cmds and len(args) >= 1:
key = args[0]
report.findings.append(AuditFindingV2(
rule_code=rule.code,
rule_name=rule.name,
level=Severity.INFO,
message=f"{cmd} 全量读取 Key '{key}' 可能返回大量数据,建议先用 MEMORY USAGE {key} 确认大小,或使用 SCAN/HSCAN/SSCAN/ZSCAN 分批读取",
))
@register_rule(AuditRuleV2(
code="REDIS-PERF-005",
name="慢查询检查",
category=RuleCategory.PERFORMANCE,
severity=Severity.WARNING,
db_type="redis",
description="检测可能导致慢查询的操作模式",
config={"slowlog_threshold_ms": 10},
message_template="{cmd} 操作可能导致慢查询(>{threshold}ms),建议避免对大量数据执行复杂操作",
))
def check_redis_perf_005(sql_clean: str, sql_upper: str, report: AuditReportV2, rule: AuditRuleV2) -> None:
"""慢查询检查。
检测可能导致慢查询的高复杂度操作。
"""
command, args = _parse_redis_command(sql_clean)
if not command:
return
cmd = command.upper()
threshold = rule.config.get("slowlog_threshold_ms", 10)
# 高复杂度命令模式检测
slow_patterns = {
"SORT": "SORT 全量排序复杂度 O(N+M*log(M))",
"SUNIONSTORE": "SUNIONSTORE 集合操作复杂度 O(N)",
"SINTERSTORE": "SINTERSTORE 集合操作复杂度 O(N*M)",
"ZUNIONSTORE": "ZUNIONSTORE 排序集合操作复杂度 O(N*K) + O(M*log(M))",
"ZINTERSTORE": "ZINTERSTORE 排序集合操作复杂度 O(N*K) + O(M*log(M))",
"ZRANGEBYLEX": "ZRANGEBYLEX 范围查询可能遍历大量成员",
"ZREMRANGEBYLEX": "ZREMRANGEBYLEX 批量删除复杂度 O(log(N)+M)",
"ZRANGEBYSCORE": "ZRANGEBYSCORE 大范围查询可能返回大量数据",
}
if cmd in slow_patterns:
report.findings.append(AuditFindingV2(
rule_code=rule.code,
rule_name=rule.name,
level=Severity.INFO,
message=f"{slow_patterns[cmd]},可能导致慢查询(>{threshold}ms),请注意监控慢查询日志(SLOWLOG GET)",
))
# KEYS 命令(已被 REDIS-KEY-007 覆盖为 ERROR,这里额外提示性能影响)
if cmd == "KEYS":
report.findings.append(AuditFindingV2(
rule_code=rule.code,
rule_name=rule.name,
level=Severity.INFO,
message=f"KEYS 命令扫描全库复杂度 O(N),在数据量大时会导致长时间阻塞,生产环境绝对禁止",
))
# 大量元素的 MGET/MSET
if cmd in ("MGET", "MSET") and len(args) > 100:
report.findings.append(AuditFindingV2(
rule_code=rule.code,
rule_name=rule.name,
level=Severity.INFO,
message=f"{cmd} 操作 {len(args)} 个 Key,批量过大可能阻塞,建议分批执行(每批不超过 100 个 Key)",
))
@register_rule(AuditRuleV2(
code="REDIS-PERF-006",
name="Lua脚本执行时间限制",
category=RuleCategory.PERFORMANCE,
severity=Severity.WARNING,
db_type="redis",
description="Lua 脚本执行时间建议不超过 5 秒",
config={"max_lua_exec_ms": 5000},
message_template="Lua 脚本 '{script_snippet}...' 可能执行时间过长,建议设置 lua-time-limit 不超过 {max_ms}ms,避免阻塞",
))
def check_redis_perf_006(sql_clean: str, sql_upper: str, report: AuditReportV2, rule: AuditRuleV2) -> None:
"""Lua 脚本执行时间限制检查。
通过脚本长度和复杂度进行静态估计。
"""
command, args = _parse_redis_command(sql_clean)
if not command:
return
cmd = command.upper()
if cmd not in ("EVAL", "EVALSHA", "FCALL", "FCALL_RO"):
return
max_ms = rule.config.get("max_lua_exec_ms", 5000)
# 如果是 EVAL,检查脚本复杂度
if cmd == "EVAL" and args:
script = args[0]
# 统计脚本中的 Redis 调用次数
redis_call_count = script.upper().count("REDIS.CALL") + script.upper().count("REDIS.PCALL")
if redis_call_count > 50:
snippet = script[:80].replace("\n", " ")
report.findings.append(AuditFindingV2(
rule_code=rule.code,
rule_name=rule.name,
level=rule.severity,
message=f"Lua 脚本包含 {redis_call_count} 次 redis.call/redis.pcall 调用,可能执行时间超过 {max_ms}ms,建议拆分脚本",
))
return
# 脚本过长
if len(script) > 5000:
snippet = script[:80].replace("\n", " ")
report.findings.append(AuditFindingV2(
rule_code=rule.code,
rule_name=rule.name,
level=Severity.INFO,
message=rule.message_template.format(script_snippet=snippet, max_ms=max_ms),
))
# FCALL 检查
if cmd in ("FCALL", "FCALL_RO") and args:
func_name = args[0]
report.findings.append(AuditFindingV2(
rule_code=rule.code,
rule_name=rule.name,
level=Severity.INFO,
message=f"Function '{func_name}' 调用需确保执行时间不超过 {max_ms}ms,建议在函数内部添加超时保护",
))
@register_rule(AuditRuleV2(
code="REDIS-PERF-007",
name="连接数限制警告",
category=RuleCategory.PERFORMANCE,
severity=Severity.WARNING,
db_type="redis",
description="监控 Redis 连接数,建议不超过 maxclients 的 80%",
config={"connection_threshold_ratio": 0.8, "maxclients_default": 10000},
message_template="当前连接数监控:建议连接数不超过 maxclients 的 {ratio}%(当前配置 maxclients={maxclients},警戒值={threshold}),请配置连接池和连接复用",
))
def check_redis_perf_007(sql_clean: str, sql_upper: str, report: AuditReportV2, rule: AuditRuleV2) -> None:
"""连接数限制警告。
当检测到 INFO CLIENTS 或 CLIENT LIST 查询时给出连接管理建议。
"""
command, args = _parse_redis_command(sql_clean)
if not command:
return
cmd = command.upper()
if cmd == "INFO" and args:
section = args[0].upper()
if section in ("CLIENTS", "ALL", "DEFAULT"):
ratio = rule.config.get("connection_threshold_ratio", 0.8)
maxclients = rule.config.get("maxclients_default", 10000)
threshold = int(maxclients * ratio)
report.findings.append(AuditFindingV2(
rule_code=rule.code,
rule_name=rule.name,
level=Severity.INFO,
message=rule.message_template.format(ratio=int(ratio * 100), maxclients=maxclients, threshold=threshold),
))
if cmd in ("CLIENT", "CONFIG") and len(args) >= 1:
subcmd = args[0].upper()
if subcmd in ("LIST", "GET", "INFO"):
ratio = rule.config.get("connection_threshold_ratio", 0.8)
maxclients = rule.config.get("maxclients_default", 10000)
threshold = int(maxclients * ratio)
report.findings.append(AuditFindingV2(
rule_code=rule.code,
rule_name=rule.name,
level=Severity.INFO,
message=f"连接管理:建议监控连接数不超过 maxclients 的 {int(ratio * 100)}%(maxclients={maxclients},警戒值={threshold}),使用连接池复用连接",
))
@register_rule(AuditRuleV2(
code="REDIS-PERF-008",
name="避免内存溢出风险命令",
category=RuleCategory.PERFORMANCE,
severity=Severity.ERROR,
db_type="redis",
description="检测可能导致 OOM 的高风险命令",
config={"oom_warning_threshold_mb": 100},
message_template="{cmd} 操作存在内存溢出风险,可能导致 Redis OOM,请确保内存充足并配置 maxmemory 和淘汰策略",
))
def check_redis_perf_008(sql_clean: str, sql_upper: str, report: AuditReportV2, rule: AuditRuleV2) -> None:
"""避免内存溢出风险命令检测。"""
command, args = _parse_redis_command(sql_clean)
if not command:
return
cmd = command.upper()
# 检测可能导致 OOM 的操作模式
oom_risk_cmds = {
"SUNIONSTORE": "SUNIONSTORE 结果集可能远大于输入集",
"SINTERSTORE": "SINTERSTORE 大量集合求交",
"ZUNIONSTORE": "ZUNIONSTORE 大量集合合并",
"PFMERGE": "PFMERGE HyperLogLog 合并",
"BITOP": "BITOP 位图操作可能产生大结果",
}
if cmd in oom_risk_cmds:
report.findings.append(AuditFindingV2(
rule_code=rule.code,
rule_name=rule.name,
level=rule.severity,
message=rule.message_template.format(cmd=cmd) + f",{oom_risk_cmds[cmd]}",
))
# 检测无 MAXLEN 的大 Stream XADD(已被 REDIS-DS-006 覆盖,这里补充 OOM 角度)
if cmd == "XADD" and len(args) >= 2:
has_maxlen = any(arg.upper() == "MAXLEN" for arg in args)
if not has_maxlen:
report.findings.append(AuditFindingV2(
rule_code=rule.code,
rule_name=rule.name,
level=Severity.INFO,
message="XADD 未限制 MAXLEN,Stream 无限增长可能导致内存溢出,建议设置 MAXLEN 或定期 XTRIM",
))
# ═══════════════════════════════════════════════
# 安全规则 (5条)
# ═══════════════════════════════════════════════
@register_rule(AuditRuleV2(
code="REDIS-SEC-001",
name="敏感管理命令需要二次确认",
category=RuleCategory.SECURITY,
severity=Severity.WARNING,
db_type="redis",
description="CONFIG/ACL/SHUTDOWN 等管理级命令属于高危操作,建议限制执行",
config={"admin_cmds": ["CONFIG", "ACL", "SHUTDOWN", "DEBUG", "MODULE", "REPLICAOF", "SLAVEOF", "SAVE", "BGSAVE", "BGREWRITEAOF"]},
message_template="命令 '{cmd}' 属于 Redis 管理级操作,请确认业务必要性后再执行",
))
def check_redis_sec_001(sql_clean: str, sql_upper: str, report: AuditReportV2, rule: AuditRuleV2) -> None:
"""管理级命令检查。
仅对 CONFIG/ACL/SHUTDOWN 等管理级命令产生警告,
常规数据操作命令(SET/DEL/HSET 等)不触发——
平台已通过实例配置管理认证,无需额外提示 AUTH。
"""
command, args = _parse_redis_command(sql_clean)
if not command:
return
cmd = command.upper()
admin_cmds = rule.config.get("admin_cmds", [])
if cmd in admin_cmds:
report.findings.append(AuditFindingV2(
rule_code=rule.code,
rule_name=rule.name,
level=rule.severity,
message=rule.message_template.format(cmd=cmd),
))
@register_rule(AuditRuleV2(
code="REDIS-SEC-002",
name="命令中不得包含密码",
category=RuleCategory.SECURITY,
severity=Severity.ERROR,
db_type="redis",
description="Redis 命令中不得明文包含密码,存在安全风险",
config={},
message_template="命令中包含明文密码,存在安全风险,请通过配置文件或环境变量传递密码,禁止在命令行中硬编码",
))
def check_redis_sec_002(sql_clean: str, sql_upper: str, report: AuditReportV2, rule: AuditRuleV2) -> None:
"""命令中不得包含密码检查。"""
command, args = _parse_redis_command(sql_clean)
if not command:
return
cmd = command.upper()
# 检查 AUTH 命令中的密码
if cmd == "AUTH" and len(args) >= 1:
password = args[-1] # 最后一个参数是密码
# 检查密码强度(简单密码检测)
if password and len(password) < 8:
report.findings.append(AuditFindingV2(
rule_code=rule.code,
rule_name=rule.name,
level=rule.severity,
message="AUTH 密码长度过短(< 8 字符),安全性不足,请使用强密码(>= 16 字符,包含大小写+数字+特殊字符)",
))
# 检查命令中是否包含常见密码模式
password_patterns = [
r"password[=:]\S+",
r"pwd[=:]\S+",
r"passwd[=:]\S+",
r"secret[=:]\S+",
]
for pattern in password_patterns:
if re.search(pattern, sql_clean, re.IGNORECASE):
report.findings.append(AuditFindingV2(
rule_code=rule.code,
rule_name=rule.name,
level=rule.severity,
message=rule.message_template,
))
return
@register_rule(AuditRuleV2(
code="REDIS-SEC-003",
name="危险命令重命名建议",
category=RuleCategory.SECURITY,
severity=Severity.INFO,
db_type="redis",
description="建议将 FLUSHDB/FLUSHALL/DEBUG/SHUTDOWN 通过 rename-command 重命名",
config={"dangerous_commands": ["DEBUG", "SHUTDOWN"]},
message_template="建议将 '{cmd}' 通过 rename-command 重命名,防止误操作",
))
def check_redis_sec_003(sql_clean: str, sql_upper: str, report: AuditReportV2, rule: AuditRuleV2) -> None:
"""危险命令重命名建议。
FLUSHDB/FLUSHALL 已由 CMD-001 硬拦截,此处仅对 DEBUG/SHUTDOWN
等管理命令给出 rename-command 配置建议,避免与 CMD-001 重复。
"""
command, args = _parse_redis_command(sql_clean)
if not command:
return
cmd = command.upper()
# 排除已由 CMD-001 拦截的 FLUSHDB/FLUSHALL/CONFIG
dangerous = rule.config.get("dangerous_commands", ["DEBUG", "SHUTDOWN"])
if cmd in dangerous:
report.findings.append(AuditFindingV2(
rule_code=rule.code,
rule_name=rule.name,
level=rule.severity,
message=rule.message_template.format(cmd=cmd),
))
@register_rule(AuditRuleV2(
code="REDIS-SEC-004",
name="禁止绑定0.0.0.0(无密码时)",
category=RuleCategory.SECURITY,
severity=Severity.ERROR,
db_type="redis",
description="Redis 无密码时不应绑定 0.0.0.0,必须绑定指定内网 IP",
message_template="Redis 配置安全风险:未设置密码时不应绑定 0.0.0.0,请绑定指定内网 IP 并设置强密码(requirepass + rename-command)",
))
def check_redis_sec_004(sql_clean: str, sql_upper: str, report: AuditReportV2, rule: AuditRuleV2) -> None:
"""禁止绑定 0.0.0.0 检查。
检测 CONFIG SET bind 命令。
"""
command, args = _parse_redis_command(sql_clean)
if not command:
return
cmd = command.upper()
if cmd == "CONFIG" and len(args) >= 3:
subcmd = args[0].upper()
param = args[1].upper()
if subcmd == "SET" and param == "BIND":
bind_value = args[2]
if bind_value == "0.0.0.0":
report.findings.append(AuditFindingV2(
rule_code=rule.code,
rule_name=rule.name,
level=rule.severity,
message=rule.message_template,
))
@register_rule(AuditRuleV2(
code="REDIS-SEC-005",
name="必须开启保护模式",
category=RuleCategory.SECURITY,
severity=Severity.ERROR,
db_type="redis",
description="Redis 必须开启保护模式(protected-mode yes),防止未授权访问",
message_template="Redis 安全:未开启保护模式存在严重安全风险,请在配置中设置 protected-mode yes 并配置 bind 内网 IP",
))
def check_redis_sec_005(sql_clean: str, sql_upper: str, report: AuditReportV2, rule: AuditRuleV2) -> None:
"""保护模式检查。
检测 CONFIG SET protected-mode 命令。
"""
command, args = _parse_redis_command(sql_clean)
if not command:
return
cmd = command.upper()
if cmd == "CONFIG" and len(args) >= 3:
subcmd = args[0].upper()
param = args[1].upper()
if subcmd == "SET" and param == "PROTECTED-MODE":
value = args[2].upper()
if value == "NO":
report.findings.append(AuditFindingV2(
rule_code=rule.code,
rule_name=rule.name,
level=rule.severity,
message=rule.message_template,
))
# 如果检测到 AUTH 失败后的操作,也提示保护模式
if cmd == "AUTH":
report.findings.append(AuditFindingV2(
rule_code=rule.code,
rule_name=rule.name,
level=Severity.INFO,
message="认证操作:请确保 Redis 已开启 protected-mode yes 并配置了强密码",
))
# ═══════════════════════════════════════════════
# 规则统计与导出
# ═══════════════════════════════════════════════
def get_redis_rule_stats() -> Dict[str, Any]:
"""获取 Redis 审核规则的统计信息。
Returns:
包含规则总数和各分类数量的字典
"""
# 导入注册表获取已注册规则
from app.services.audit_engine_v2 import _RULE_REGISTRY, AuditRuleV2
redis_rules = [
(code, rule, func)
for code, (rule, func) in _RULE_REGISTRY.items()
if code.startswith("REDIS-")
]
categories: Dict[str, int] = {}
severities: Dict[str, int] = {}
for code, rule, _ in redis_rules:
cat = rule.category.value if hasattr(rule.category, "value") else str(rule.category)
categories[cat] = categories.get(cat, 0) + 1
sev = rule.severity.value if hasattr(rule.severity, "value") else str(rule.severity)
severities[sev] = severities.get(sev, 0) + 1
return {
"total": len(redis_rules),
"by_category": categories,
"by_severity": severities,
"rules": sorted([code for code, _, _ in redis_rules]),
}
def print_redis_rule_summary() -> None:
"""打印 Redis 审核规则汇总信息。"""
stats = get_redis_rule_stats()
print("=" * 60)
print("Redis 审核规则引擎 - 规则汇总")
print("=" * 60)
print(f"规则总数: {stats['total']} 条")
print()
print("按分类统计:")
for cat, count in sorted(stats["by_category"].items()):
print(f" {cat}: {count} 条")
print()
print("按严重级别统计:")
for sev, count in sorted(stats["by_severity"].items()):
print(f" {sev}: {count} 条")
print()
print("规则编码列表:")
for code in stats["rules"]:
print(f" {code}")
print("=" * 60)
# 模块加载时自动打印规则摘要(仅在直接运行时)
if __name__ == "__main__":
print_redis_rule_summary()