436 lines
12 KiB
Go
436 lines
12 KiB
Go
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: "权限变更操作"},
|
|
}
|
|
}
|