后端新增 (Step 1): - mysql_audit_rules.py: 63条 MySQL审核规则 (DDL 36 + DML 9 + 命名 5 + 性能 5 + 安全 5) - redis_audit_rules.py: 43条 Redis审核规则 (KEY管理 10 + 命令安全 12 + 数据结构 8 + 性能 8 + 安全 5) - mongodb_audit_rules.py: 42条 MongoDB审核规则 (集合管理 + 查询安全 + 聚合管道 + 写入安全 + 性能) - postgres_audit_rules.py: 45条 PostgreSQL审核规则 (DDL 18 + DML 8 + DQL 6 + 安全 6 + 性能 7) 后端合并 (Step 2): - audit_engine_v2.py: 新增4个DB规则模块import (100+→200+规则) - audit_rule_seeds.py: 新增30条MongoDB种子规则 + 别名映射 - audit_rule.py: 新增数据库类型别名 (postgresql→postgres, pg→postgres, mongo→mongodb) 前端优化 (Step 4) — 16页面全覆盖: - InstancePage, WorkflowPage, TicketsPage, TicketCreatePage, TicketDetailPage - UsersPage, SlowQueryPage, DataDictionaryPage, Dashboard - InstanceGroupPage, AuditLogsPage, QueriesPage, AuditRulesPage, RolesPage - useAuth.tsx, SqlEditor.tsx 后端测试 895/895 通过, 前端 tsc --noEmit 通过
1864 lines
71 KiB
Python
1864 lines
71 KiB
Python
"""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
|
||
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.ERROR,
|
||
db_type="redis",
|
||
description="非只读命令必须执行 AUTH 认证后才能执行",
|
||
config={"require_auth": True, "allowed_cmds_without_auth": ["PING", "AUTH", "ECHO", "QUIT", "HELLO"]},
|
||
message_template="命令 '{cmd}' 需要认证后才能执行,请先执行 AUTH password 进行认证",
|
||
))
|
||
def check_redis_sec_001(sql_clean: str, sql_upper: str, report: AuditReportV2, rule: AuditRuleV2) -> None:
|
||
"""必须认证检查。
|
||
|
||
提示执行敏感命令前需要 AUTH 认证。
|
||
"""
|
||
command, args = _parse_redis_command(sql_clean)
|
||
if not command:
|
||
return
|
||
cmd = command.upper()
|
||
|
||
allowed = rule.config.get("allowed_cmds_without_auth", ["PING", "AUTH", "ECHO", "QUIT", "HELLO"])
|
||
if cmd in allowed:
|
||
return
|
||
|
||
# 只对写入和敏感命令要求认证
|
||
sensitive_cmds = {
|
||
"SET", "DEL", "FLUSHDB", "FLUSHALL", "CONFIG", "ACL", "RENAME",
|
||
"MIGRATE", "RESTORE", "SHUTDOWN", "DEBUG", "MODULE", "REPLICAOF",
|
||
"SLAVEOF", "SAVE", "BGSAVE", "BGREWRITEAOF",
|
||
}
|
||
if cmd in sensitive_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.WARNING,
|
||
db_type="redis",
|
||
description="建议将危险命令(FLUSHDB/FLUSHALL/CONFIG/DEBUG)通过 rename-command 重命名",
|
||
config={"dangerous_commands": ["FLUSHDB", "FLUSHALL", "CONFIG", "DEBUG", "SHUTDOWN"]},
|
||
message_template="危险命令 '{cmd}' 应通过 rename-command 在配置中重命名,防止误操作或恶意执行",
|
||
))
|
||
def check_redis_sec_003(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()
|
||
|
||
dangerous = rule.config.get("dangerous_commands", ["FLUSHDB", "FLUSHALL", "CONFIG", "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()
|