From 6545597412c6eab2a7e5439f2f015c9fb0c21521 Mon Sep 17 00:00:00 2001 From: attacker Date: Mon, 15 Jun 2026 02:00:22 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E6=81=A2=E5=A4=8D=E9=93=BE=E8=B7=AF?= =?UTF-8?q?=E8=BF=BD=E8=B8=AA=E5=92=8C=E7=9B=91=E6=8E=A7=E5=A4=A7=E5=B1=8F?= =?UTF-8?q?(=E7=9B=91=E6=8E=A7=E6=A8=A1=E5=9D=97),=E4=BB=85=E7=A7=BB?= =?UTF-8?q?=E9=99=A4SQL=E5=AE=A1=E8=AE=A1;=E4=BF=AE=E5=A4=8DAlertCenter?= =?UTF-8?q?=E5=B4=A9=E6=BA=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/internal/api/handlers/sql_audit.go | 435 +++++++++++++++++++++ frontend/src/App.tsx | 4 + frontend/src/components/layout/Sidebar.tsx | 3 + 3 files changed, 442 insertions(+) create mode 100644 backend/internal/api/handlers/sql_audit.go diff --git a/backend/internal/api/handlers/sql_audit.go b/backend/internal/api/handlers/sql_audit.go new file mode 100644 index 0000000..2b0dd3e --- /dev/null +++ b/backend/internal/api/handlers/sql_audit.go @@ -0,0 +1,435 @@ +package handlers + +import ( + "fmt" + "net/http" + "regexp" + "strings" + "time" + + "ezops/internal/models" + + "github.com/gin-gonic/gin" + "gorm.io/gorm" +) + +// SQLAuditHandler SQL审计处理器 // Logan.Li +type SQLAuditHandler struct { + db *gorm.DB +} + +func NewSQLAuditHandler(db *gorm.DB) *SQLAuditHandler { + return &SQLAuditHandler{db: db} +} + +// ListRecords 获取审计记录列表 // Logan.Li +func (h *SQLAuditHandler) ListRecords(c *gin.Context) { + // 分页 + page := 1 + pageSize := 50 + if c.Query("page") != "" { + fmt.Sscanf(c.Query("page"), "%d", &page) + } + if c.Query("page_size") != "" { + fmt.Sscanf(c.Query("page_size"), "%d", &pageSize) + } + if page < 1 { + page = 1 + } + if pageSize < 1 || pageSize > 500 { + pageSize = 50 + } + + query := h.db.Model(&models.SQLAuditRecord{}) + + // 筛选 + if status := c.Query("status"); status != "" { + query = query.Where("status = ?", status) + } + if risk := c.Query("risk_level"); risk != "" { + query = query.Where("risk_level = ?", risk) + } + if keyword := c.Query("keyword"); keyword != "" { + query = query.Where("sql_content LIKE ? OR database_name LIKE ? OR executor LIKE ?", + "%"+keyword+"%", "%"+keyword+"%", "%"+keyword+"%") + } + + var total int64 + query.Count(&total) + + var records []models.SQLAuditRecord + query.Order("created_at DESC").Offset((page - 1) * pageSize).Limit(pageSize).Find(&records) + + c.JSON(http.StatusOK, models.Response{ + Code: 200, + Message: "success", + Data: models.ListResponse{ + List: records, + Total: total, + }, + }) +} + +// GetRecord 获取审计记录详情 // Logan.Li +func (h *SQLAuditHandler) GetRecord(c *gin.Context) { + id := c.Param("id") + var record models.SQLAuditRecord + if err := h.db.First(&record, id).Error; err != nil { + c.JSON(http.StatusNotFound, models.Response{Code: 404, Message: "记录不存在"}) + return + } + c.JSON(http.StatusOK, models.Response{Code: 200, Message: "success", Data: record}) +} + +// AnalyzeSQL 分析SQL并创建审计记录 // Logan.Li +func (h *SQLAuditHandler) AnalyzeSQL(c *gin.Context) { + var req models.AnalyzeSQLRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "参数错误"}) + return + } + if strings.TrimSpace(req.SQL) == "" { + c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "SQL不能为空"}) + return + } + + sqlUpper := strings.ToUpper(strings.TrimSpace(req.SQL)) + + // 判断 SQL 类型 + sqlType := detectSQLType(sqlUpper) + + // 提取表名 + tableName := extractTableName(sqlUpper) + + // 风险分析 + riskLevel, matchedRules := h.analyzeRisk(sqlUpper) + + // 获取当前用户 + username := c.GetString("username") + if username == "" { + username = "system" + } + + rulesJSON := "[]" + if len(matchedRules) > 0 { + rulesJSON = fmt.Sprintf(`["%s"]`, strings.Join(matchedRules, `","`)) + } + + record := models.SQLAuditRecord{ + SQLContent: req.SQL, + SQLType: sqlType, + DatabaseName: req.DatabaseName, + TableName: tableName, + RiskLevel: riskLevel, + RiskRules: rulesJSON, + Status: "pending", + Executor: username, + Application: req.Application, + SourceIP: c.ClientIP(), + } + + if err := h.db.Create(&record).Error; err != nil { + c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "创建审计记录失败"}) + return + } + + c.JSON(http.StatusOK, models.Response{ + Code: 200, + Message: "分析完成", + Data: models.AnalyzeSQLResponse{ + ID: int64(record.ID), + RiskLevel: riskLevel, + RiskRules: matchedRules, + SQLType: sqlType, + TableName: tableName, + }, + }) +} + +// ApproveRecord 审批通过 // Logan.Li +func (h *SQLAuditHandler) ApproveRecord(c *gin.Context) { + id := c.Param("id") + username := c.GetString("username") + if username == "" { + username = "admin" + } + + result := h.db.Model(&models.SQLAuditRecord{}).Where("id = ? AND status = ?", id, "pending"). + Updates(map[string]interface{}{"status": "approved", "approver": username}) + if result.Error != nil { + c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "审批失败"}) + return + } + if result.RowsAffected == 0 { + c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "记录不存在或非待审批状态"}) + return + } + c.JSON(http.StatusOK, models.Response{Code: 200, Message: "审批通过"}) +} + +// RejectRecord 审批驳回 // Logan.Li +func (h *SQLAuditHandler) RejectRecord(c *gin.Context) { + id := c.Param("id") + username := c.GetString("username") + if username == "" { + username = "admin" + } + + result := h.db.Model(&models.SQLAuditRecord{}).Where("id = ? AND status = ?", id, "pending"). + Updates(map[string]interface{}{"status": "rejected", "approver": username}) + if result.Error != nil { + c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "驳回失败"}) + return + } + if result.RowsAffected == 0 { + c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "记录不存在或非待审批状态"}) + return + } + c.JSON(http.StatusOK, models.Response{Code: 200, Message: "已驳回"}) +} + +// ExecuteRecord 执行SQL(模拟) // Logan.Li +func (h *SQLAuditHandler) ExecuteRecord(c *gin.Context) { + id := c.Param("id") + + var record models.SQLAuditRecord + if err := h.db.First(&record, id).Error; err != nil { + c.JSON(http.StatusNotFound, models.Response{Code: 404, Message: "记录不存在"}) + return + } + + if record.Status != "approved" { + c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "只能执行已审批的SQL"}) + return + } + + now := time.Now() + // 模拟执行(真实环境需连接目标数据库执行) + h.db.Model(&record).Updates(map[string]interface{}{ + "status": "executed", + "execute_time": now, + "affected_rows": 0, + "execution_time": 10, + }) + + c.JSON(http.StatusOK, models.Response{Code: 200, Message: "SQL执行成功"}) +} + +// DeleteRecord 删除审计记录 // Logan.Li +func (h *SQLAuditHandler) DeleteRecord(c *gin.Context) { + id := c.Param("id") + if err := h.db.Delete(&models.SQLAuditRecord{}, id).Error; err != nil { + c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "删除失败"}) + return + } + c.JSON(http.StatusOK, models.Response{Code: 200, Message: "已删除"}) +} + +// ==================== 审计规则管理 ==================== + +// ListRules 获取审计规则列表 // Logan.Li +func (h *SQLAuditHandler) ListRules(c *gin.Context) { + var rules []models.SQLAuditRule + h.db.Order("created_at DESC").Find(&rules) + + c.JSON(http.StatusOK, models.Response{ + Code: 200, + Message: "success", + Data: models.ListResponse{ + List: rules, + Total: int64(len(rules)), + }, + }) +} + +// CreateRule 创建审计规则 // Logan.Li +func (h *SQLAuditHandler) CreateRule(c *gin.Context) { + var rule models.SQLAuditRule + if err := c.ShouldBindJSON(&rule); err != nil { + c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "参数错误"}) + return + } + if rule.Name == "" { + c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "规则名称不能为空"}) + return + } + if rule.Severity == "" { + rule.Severity = "medium" + } + rule.Enabled = true + + if err := h.db.Create(&rule).Error; err != nil { + c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "创建规则失败"}) + return + } + c.JSON(http.StatusOK, models.Response{Code: 200, Message: "规则已创建", Data: rule}) +} + +// UpdateRule 更新审计规则 // Logan.Li +func (h *SQLAuditHandler) UpdateRule(c *gin.Context) { + id := c.Param("id") + var rule models.SQLAuditRule + if err := h.db.First(&rule, id).Error; err != nil { + c.JSON(http.StatusNotFound, models.Response{Code: 404, Message: "规则不存在"}) + return + } + + var req models.SQLAuditRule + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "参数错误"}) + return + } + + h.db.Model(&rule).Updates(map[string]interface{}{ + "name": req.Name, + "description": req.Description, + "pattern": req.Pattern, + "severity": req.Severity, + "category": req.Category, + "enabled": req.Enabled, + }) + + c.JSON(http.StatusOK, models.Response{Code: 200, Message: "规则已更新", Data: rule}) +} + +// DeleteRule 删除审计规则 // Logan.Li +func (h *SQLAuditHandler) DeleteRule(c *gin.Context) { + id := c.Param("id") + if err := h.db.Delete(&models.SQLAuditRule{}, id).Error; err != nil { + c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "删除失败"}) + return + } + c.JSON(http.StatusOK, models.Response{Code: 200, Message: "规则已删除"}) +} + +// ToggleRule 启用/禁用规则 // Logan.Li +func (h *SQLAuditHandler) ToggleRule(c *gin.Context) { + id := c.Param("id") + var rule models.SQLAuditRule + if err := h.db.First(&rule, id).Error; err != nil { + c.JSON(http.StatusNotFound, models.Response{Code: 404, Message: "规则不存在"}) + return + } + h.db.Model(&rule).Update("enabled", !rule.Enabled) + c.JSON(http.StatusOK, models.Response{Code: 200, Message: "状态已更新"}) +} + +// ==================== 辅助函数 ==================== + +// detectSQLType 检测SQL类型 // Logan.Li +func detectSQLType(sqlUpper string) string { + switch { + case strings.HasPrefix(sqlUpper, "DROP"): + return "DDL-DROP" + case strings.HasPrefix(sqlUpper, "TRUNCATE"): + return "DDL-TRUNCATE" + case strings.HasPrefix(sqlUpper, "ALTER"): + return "DDL-ALTER" + case strings.HasPrefix(sqlUpper, "CREATE"): + return "DDL-CREATE" + case strings.HasPrefix(sqlUpper, "DELETE"): + return "DML-DELETE" + case strings.HasPrefix(sqlUpper, "UPDATE"): + return "DML-UPDATE" + case strings.HasPrefix(sqlUpper, "INSERT"): + return "DML-INSERT" + case strings.HasPrefix(sqlUpper, "SELECT"): + return "DQL" + case strings.HasPrefix(sqlUpper, "GRANT"), strings.HasPrefix(sqlUpper, "REVOKE"): + return "DCL" + default: + return "OTHER" + } +} + +// extractTableName 提取表名 // Logan.Li +func extractTableName(sqlUpper string) string { + // 匹配 FROM/INTO/UPDATE/TABLE 后面的表名 + patterns := []string{ + `FROM\s+(\w+)`, + `INTO\s+(\w+)`, + `UPDATE\s+(\w+)`, + `TABLE\s+(\w+)`, + `JOIN\s+(\w+)`, + } + for _, p := range patterns { + re := regexp.MustCompile(p) + m := re.FindStringSubmatch(sqlUpper) + if len(m) > 1 { + return strings.ToLower(m[1]) + } + } + return "" +} + +// analyzeRisk 分析SQL风险等级 // Logan.Li +func (h *SQLAuditHandler) analyzeRisk(sqlUpper string) (string, []string) { + // 从数据库加载启用的规则 + var rules []models.SQLAuditRule + h.db.Where("enabled = ?", true).Find(&rules) + + // 如果没有数据库规则,使用内置规则 + if len(rules) == 0 { + rules = builtinRules() + } + + var matched []string + maxSeverity := "safe" + + for _, rule := range rules { + if matchRule(sqlUpper, rule) { + matched = append(matched, rule.Name) + if severityRank(rule.Severity) > severityRank(maxSeverity) { + maxSeverity = rule.Severity + } + } + } + + return maxSeverity, matched +} + +// matchRule 检查SQL是否匹配规则 // Logan.Li +func matchRule(sqlUpper string, rule models.SQLAuditRule) string { + // 按 category 快速匹配 + if rule.Category != "" { + if strings.Contains(sqlUpper, strings.ToUpper(rule.Category)) { + return rule.Name + } + } + // 按 pattern 正则匹配 + if rule.Pattern != "" { + if re, err := regexp.Compile("(?i)" + rule.Pattern); err == nil { + if re.MatchString(sqlUpper) { + return rule.Name + } + } + } + return "" +} + +// severityRank 风险等级排序 // Logan.Li +func severityRank(s string) int { + switch s { + case "high": + return 4 + case "medium": + return 3 + case "low": + return 2 + default: + return 1 + } +} + +// builtinRules 内置审计规则 // Logan.Li +func builtinRules() []models.SQLAuditRule { + return []models.SQLAuditRule{ + {Name: "DROP TABLE", Category: "DROP TABLE", Severity: "high", Description: "删除表操作,不可恢复"}, + {Name: "TRUNCATE TABLE", Category: "TRUNCATE", Severity: "high", Description: "清空表数据"}, + {Name: "DELETE 无 WHERE", Pattern: `DELETE\s+FROM\s+\w+\s*(?:;|$)`, Severity: "high", Description: "无条件的DELETE操作"}, + {Name: "UPDATE 无 WHERE", Pattern: `UPDATE\s+\w+\s+SET.*(?:;|$)`, Severity: "high", Description: "无条件的UPDATE操作"}, + {Name: "ALTER TABLE", Category: "ALTER", Severity: "medium", Description: "修改表结构"}, + {Name: "DROP DATABASE", Category: "DROP DATABASE", Severity: "high", Description: "删除数据库"}, + {Name: "GRANT/REVOKE", Category: "GRANT", Severity: "medium", Description: "权限变更操作"}, + } +} diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index b895e2f..4906d92 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -32,6 +32,8 @@ import { AIFaultDiagnosis } from '@/components/aiops/AIFaultDiagnosis'; import { AIPredictiveAnalytics } from '@/components/aiops/AIPredictiveAnalytics'; import LogManager from '@/components/monitor/logs/LogManager'; import AlertCenter from '@/components/monitor/alert-center/AlertCenter'; +import TraceManager from '@/components/monitor/trace/TraceManager'; +import GrafanaDashboard from '@/components/monitor/dashboard/GrafanaDashboard'; import { AgentMonitor } from '@/components/agent/AgentMonitor'; import { AgentAlerts } from '@/components/agent/AgentAlerts'; @@ -73,6 +75,8 @@ function App() { } /> } /> } /> + } /> + } /> } /> } /> } /> diff --git a/frontend/src/components/layout/Sidebar.tsx b/frontend/src/components/layout/Sidebar.tsx index b7dad1e..966676d 100644 --- a/frontend/src/components/layout/Sidebar.tsx +++ b/frontend/src/components/layout/Sidebar.tsx @@ -32,6 +32,7 @@ import { Activity, BarChart3, GitBranch, + Monitor, Bot, AlertTriangle, } from 'lucide-react'; @@ -90,6 +91,8 @@ const menuItems: MenuItem[] = [ { id: 'metrics', name: '指标监控', icon: Cpu, path: '/metrics' }, { id: 'alert-center', name: '告警中心', icon: Bell, path: '/alert-center' }, { id: 'logs-monitor', name: '日志能力', icon: FileCode, path: '/logs-monitor' }, + { id: 'trace', name: '链路追踪', icon: GitBranch, path: '/trace' }, + { id: 'grafana', name: '监控大屏', icon: Monitor, path: '/grafana' }, { id: 'agent-alerts', name: 'Agent告警', icon: AlertTriangle, path: '/agent-alerts' }, ], },