package aitools import ( "fmt" "strings" "unicode" "opsxdb/server/internal/db" ) // redisReadCmds P0 纯只读允许的 Redis 命令(对齐现有 execRedis 读路径)。 // Logan.Li var redisReadCmds = map[string]bool{ "PING": true, "GET": true, "MGET": true, "TYPE": true, "TTL": true, "PTTL": true, "EXISTS": true, "SCAN": true, "HGET": true, "HGETALL": true, "HLEN": true, "HKEYS": true, "HVALS": true, "LRANGE": true, "LLEN": true, "LINDEX": true, "SMEMBERS": true, "SCARD": true, "SISMEMBER": true, "ZRANGE": true, "ZCARD": true, "ZSCORE": true, "ZRANK": true, "STRLEN": true, "DBSIZE": true, "INFO": true, "OBJECT": true, "MEMORY": true, } // redisDeniedAlways P0 明确拒绝(即便将来 read_write 也需确认;此处直接拒)。 var redisDeniedAlways = map[string]bool{ "FLUSHALL": true, "FLUSHDB": true, "KEYS": true, "CONFIG": true, } // IsReadOnlySQL P0:仅允许读类 SQL。INSERT/UPDATE/DELETE/DDL 一律 false。 // WITH 仅当体中无写动词且含 SELECT 时放行。复用 CheckDangerousSQL 作二次拦截。 func IsReadOnlySQL(sqlText string) (bool, string) { s := strings.TrimSpace(stripLeadComments(sqlText)) if s == "" { return false, "empty sql" } up := strings.ToUpper(s) // 去掉开头空白后的首词 first := firstWord(up) switch first { case "SELECT", "SHOW", "DESCRIBE", "DESC", "EXPLAIN", "PRAGMA": // ok case "WITH": // WITH … SELECT 可读;WITH … INSERT/UPDATE/DELETE 拒 if hasWriteVerb(up) { return false, "WITH statement contains write verb (P0 read-only)" } if !strings.Contains(up, "SELECT") { return false, "WITH without SELECT is not allowed in read-only mode" } default: return false, fmt.Sprintf("not a read-only statement (starts with %s); P0 MCP is read-only", first) } if hits := db.CheckDangerousSQL("", sqlText); len(hits) > 0 { return false, "dangerous sql blocked: " + strings.Join(hits, "; ") } return true, "" } func hasWriteVerb(up string) bool { for _, v := range []string{" INSERT ", " UPDATE ", " DELETE ", " DROP ", " ALTER ", " CREATE ", " TRUNCATE ", " REPLACE ", " MERGE ", " GRANT ", " REVOKE ", " CALL ", " EXEC ", " EXECUTE "} { if strings.Contains(" "+up+" ", v) { return true } } // 行首写动词 for _, v := range []string{"INSERT", "UPDATE", "DELETE", "DROP", "ALTER", "CREATE", "TRUNCATE", "REPLACE", "MERGE"} { if strings.HasPrefix(up, v+" ") || up == v { return true } } return false } func firstWord(up string) string { up = strings.TrimSpace(up) i := 0 for i < len(up) && (unicode.IsLetter(rune(up[i])) || up[i] == '_') { i++ } if i == 0 { return "" } return up[:i] } func stripLeadComments(sql string) string { s := strings.TrimSpace(sql) for { if strings.HasPrefix(s, "--") { if i := strings.IndexByte(s, '\n'); i >= 0 { s = strings.TrimSpace(s[i+1:]) continue } return "" } if strings.HasPrefix(s, "/*") { if i := strings.Index(s, "*/"); i >= 0 { s = strings.TrimSpace(s[i+2:]) continue } return "" } break } return s } // CheckRedisReadOnly P0:仅白名单读命令;FLUSH*/KEYS/CONFIG 等一律拒。 func CheckRedisReadOnly(commandLine string) (cmd string, err error) { fields := strings.Fields(strings.TrimSpace(commandLine)) if len(fields) == 0 { return "", fmt.Errorf("empty redis command") } cmd = strings.ToUpper(fields[0]) if redisDeniedAlways[cmd] { return cmd, fmt.Errorf("redis command denied in P0 read-only: %s", cmd) } if !redisReadCmds[cmd] { return cmd, fmt.Errorf("redis command not allowed in P0 read-only: %s (writes require P1)", cmd) } return cmd, nil }