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

+137 -52
View File
@@ -5,6 +5,8 @@ import (
"net/http"
"strings"
"sql-platform/internal/model"
"github.com/gin-gonic/gin"
)
@@ -13,6 +15,7 @@ import (
func AuditTicketSQL(c *gin.Context) {
var input struct {
SQLContent string `json:"sql_content" binding:"required"`
InstanceID int `json:"instance_id"`
DBType string `json:"db_type"`
Database string `json:"database"`
}
@@ -21,78 +24,160 @@ func AuditTicketSQL(c *gin.Context) {
return
}
// Basic SQL audit rules
var warnings []map[string]interface{}
sql := strings.ToUpper(input.SQLContent)
// Rule: No WHERE clause on UPDATE/DELETE
if (strings.HasPrefix(sql, "UPDATE") || strings.HasPrefix(sql, "DELETE")) && !strings.Contains(sql, "WHERE") {
warnings = append(warnings, map[string]interface{}{
"rule": "no_where_clause",
"severity": "high",
"message": "UPDATE/DELETE without WHERE clause affects all rows",
})
// Resolve db_type from instance_id if not provided
dbType := input.DBType
if dbType == "" && input.InstanceID > 0 {
var instance model.Instance
if err := model.DB.Select("db_type").First(&instance, input.InstanceID).Error; err == nil {
dbType = instance.DBType
}
}
// Rule: SELECT *
if strings.Contains(sql, "SELECT *") {
warnings = append(warnings, map[string]interface{}{
"rule": "select_star",
"severity": "low",
"message": "SELECT * may return unnecessary columns",
})
var findings []map[string]interface{}
sql := strings.ToUpper(strings.TrimSpace(input.SQLContent))
if dbType == "redis" {
// Redis command audit
findings = auditRedisCommand(sql)
} else {
// SQL audit rules
findings = auditSQL(sql)
}
// Rule: No LIMIT on SELECT (for large tables)
if strings.HasPrefix(sql, "SELECT") && !strings.Contains(sql, "LIMIT") {
warnings = append(warnings, map[string]interface{}{
"rule": "no_limit",
"severity": "medium",
"message": "SELECT without LIMIT may return excessive rows",
})
}
// Rule: DROP TABLE
if strings.Contains(sql, "DROP TABLE") {
warnings = append(warnings, map[string]interface{}{
"rule": "drop_table",
"severity": "critical",
"message": "DROP TABLE is a destructive operation",
})
}
// Rule: TRUNCATE
if strings.Contains(sql, "TRUNCATE") {
warnings = append(warnings, map[string]interface{}{
"rule": "truncate",
"severity": "critical",
"message": "TRUNCATE is a destructive operation",
})
}
if warnings == nil {
warnings = []map[string]interface{}{}
if findings == nil {
findings = []map[string]interface{}{}
}
// Determine risk level
riskLevel := "low"
for _, w := range warnings {
if w["severity"] == "critical" {
for _, f := range findings {
sev, _ := f["level"].(string)
if sev == "error" {
riskLevel = "high"
break
}
if w["severity"] == "high" {
if sev == "warning" {
riskLevel = "medium"
}
}
c.JSON(http.StatusOK, gin.H{
"risk_level": riskLevel,
"warnings": warnings,
"passed": len(warnings) == 0,
"findings": findings,
"passed": len(findings) == 0,
})
}
func auditSQL(sql string) []map[string]interface{} {
var findings []map[string]interface{}
// Rule: No WHERE clause on UPDATE/DELETE
if (strings.HasPrefix(sql, "UPDATE") || strings.HasPrefix(sql, "DELETE")) && !strings.Contains(sql, "WHERE") {
findings = append(findings, map[string]interface{}{
"rule": "no_where_clause", "level": "error",
"message": "UPDATE/DELETE without WHERE clause affects all rows",
})
}
// Rule: SELECT *
if strings.Contains(sql, "SELECT *") {
findings = append(findings, map[string]interface{}{
"rule": "select_star", "level": "info",
"message": "SELECT * may return unnecessary columns",
})
}
// Rule: No LIMIT on SELECT (for large tables)
if strings.HasPrefix(sql, "SELECT") && !strings.Contains(sql, "LIMIT") {
findings = append(findings, map[string]interface{}{
"rule": "no_limit", "level": "warning",
"message": "SELECT without LIMIT may return excessive rows",
})
}
// Rule: DROP TABLE
if strings.Contains(sql, "DROP TABLE") {
findings = append(findings, map[string]interface{}{
"rule": "drop_table", "level": "error",
"message": "DROP TABLE is a destructive operation",
})
}
// Rule: TRUNCATE
if strings.Contains(sql, "TRUNCATE") {
findings = append(findings, map[string]interface{}{
"rule": "truncate", "level": "error",
"message": "TRUNCATE is a destructive operation",
})
}
return findings
}
func auditRedisCommand(sql string) []map[string]interface{} {
var findings []map[string]interface{}
// Extract command name
parts := strings.Fields(sql)
if len(parts) == 0 {
return findings
}
cmd := parts[0]
// Read-only commands are safe
readOnly := map[string]bool{
"GET": true, "MGET": true, "HGET": true, "HGETALL": true,
"LRANGE": true, "SMEMBERS": true, "ZRANGE": true, "ZSCORE": true,
"TYPE": true, "TTL": true, "PTTL": true, "EXISTS": true, "KEYS": true,
"INFO": true, "DBSIZE": true, "SCAN": true, "STRLEN": true,
"LLEN": true, "SCARD": true, "ZCARD": true, "HLEN": true,
"HKEYS": true, "HVALS": true, "PING": true, "CONFIG": true,
"CLIENT": true, "SLOWLOG": true, "CLUSTER": true, "MEMORY": true,
"COMMAND": true, "SRANDMEMBER": true, "RANDOMKEY": true,
}
if readOnly[cmd] {
// Read-only: always safe
return findings
}
// Write commands — always need approval
writeDangerous := map[string]bool{
"FLUSHALL": true, "FLUSHDB": true, "DEBUG": true,
}
if writeDangerous[cmd] {
findings = append(findings, map[string]interface{}{
"rule": "redis_dangerous_command", "level": "error",
"message": cmd + " is a dangerous operation",
})
return findings
}
// General write commands
writeCmds := map[string]bool{
"SET": true, "DEL": true, "HSET": true, "HDEL": true,
"LPUSH": true, "RPUSH": true, "LPOP": true, "RPOP": true,
"SADD": true, "SREM": true, "ZADD": true, "ZREM": true,
"EXPIRE": true, "TTL": true, "RENAME": true, "MOVE": true,
"MSET": true, "SETNX": true, "SETEX": true, "INCR": true,
"INCRBY": true, "DECR": true, "DECRBY": true, "APPEND": true,
"PERSIST": true, "PEXPIRE": true, "EXPIREAT": true,
}
if writeCmds[cmd] {
findings = append(findings, map[string]interface{}{
"rule": "redis_write_command", "level": "warning",
"message": cmd + " is a write operation, requires approval",
})
} else {
findings = append(findings, map[string]interface{}{
"rule": "redis_unknown_command", "level": "info",
"message": cmd + " is not in known command list",
})
}
return findings
}
// POST /api/v1/sql-assistant/format
func SQLFormat(c *gin.Context) {
var input struct {