feat: ZRANGE WITHSCORES JSON自动解析为结构化表格 + Redis审计规则 + 前端安全检查

- ZRANGE WITHSCORES 自动检测JSON member,展开为 open/close/high/low/volume 等独立列
- Redis 审计: 只读命令安全,写命令warning,FLUSHALL等危险命令error
- TicketCreatePage: auditResult.findings 添加 undefined 安全检查
- 审计API支持 instance_id 自动解析 db_type
This commit is contained in:
logan committed 2026-05-31 02:07:10 +08:00
1 parent 9c5cb04cfa
commit 24c633d9f0
3 files changed
+255 -54

No files matched your search

+117 -1
View File
@@ -13,6 +13,7 @@ import (
"sql-platform/internal/middleware"
"strings"
"strconv"
"sort"
"encoding/json"
"bytes"
"regexp"
@@ -869,7 +870,122 @@ func executeRedisQuery(instance *model.Instance, dbSelect string, command string
switch v := result.(type) {
case []interface{}:
// Array result (MGET, LRANGE, etc.)
// ZRANGE WITHSCORES: Do() returns [[member1, score1], [member2, score2], ...]
// Each element is a []interface{} pair
if (cmd == "ZRANGE" || cmd == "ZRANGEBYSCORE") && len(v) >= 1 {
withScores := false
if pair, ok := v[0].([]interface{}); ok && len(pair) == 2 {
// Check if second element is numeric (score)
switch pair[1].(type) {
case float64, int64, string:
withScores = true
}
}
if withScores {
type jsonEntry struct {
Member string
Score float64
Fields map[string]interface{}
}
entries := make([]jsonEntry, 0, len(v))
allKeys := map[string]bool{}
for _, item := range v {
pair, ok := item.([]interface{})
if !ok || len(pair) < 2 {
continue
}
member := fmt.Sprintf("%v", pair[0])
var score float64
switch s := pair[1].(type) {
case float64:
score = s
case int64:
score = float64(s)
case string:
score, _ = strconv.ParseFloat(s, 64)
}
entry := jsonEntry{Member: member, Score: score}
var parsed interface{}
if json.Unmarshal([]byte(member), &parsed) == nil {
if m, ok := parsed.(map[string]interface{}); ok {
entry.Fields = m
for k := range m {
allKeys[k] = true
}
}
}
entries = append(entries, entry)
}
if len(allKeys) > 0 {
// JSON members → expand into table columns
sortedKeys := make([]string, 0, len(allKeys))
for k := range allKeys {
sortedKeys = append(sortedKeys, k)
}
sort.Strings(sortedKeys)
cols := []string{"#", "timestamp"}
cols = append(cols, sortedKeys...)
for idx, entry := range entries {
row := map[string]interface{}{
"#": idx + 1,
"timestamp": int64(entry.Score),
}
for _, k := range sortedKeys {
if entry.Fields != nil {
if fv, ok := entry.Fields[k]; ok {
row[k] = fv
} else {
row[k] = nil
}
} else {
row[k] = nil
}
}
rows = append(rows, row)
}
columns = cols
} else {
// Plain members with scores
for idx, item := range v {
pair, ok := item.([]interface{})
if !ok || len(pair) < 2 {
continue
}
member := fmt.Sprintf("%v", pair[0])
var score float64
switch s := pair[1].(type) {
case float64:
score = s
case int64:
score = float64(s)
case string:
score, _ = strconv.ParseFloat(s, 64)
}
rows = append(rows, map[string]interface{}{
"#": idx + 1,
"member": member,
"timestamp": int64(score),
})
}
columns = []string{"#", "member", "timestamp"}
}
total := len(rows)
return gin.H{
"columns": columns,
"rows": rows,
"total": total,
"count": total,
}, nil
}
}
// Array result (MGET, LRANGE, ZRANGE without WITHSCORES, etc.)
if cmd == "MGET" {
for i, val := range v {
rows = append(rows, map[string]interface{}{