fix: 恢复链路追踪和监控大屏(监控模块),仅移除SQL审计;修复AlertCenter崩溃
This commit is contained in:
1 parent
df697e0b25
commit
6545597412
3 files changed
+442
No files matched your search
@@ -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: "权限变更操作"},
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user