refactor: 清理SQL审计全栈死代码 (删除孤立组件+handler+model+路由+mock+API定义, 共-1278行)
SQL审计菜单此前已从侧边栏移除,但死代码残留(违反step5清理清单): - 删除 frontend/components/audit/SQLAuditManager.tsx (629行, 无任何外部引用) - 删除 backend/handlers/sql_audit.go (435行) + backend/models/sql_audit.go (64行) - 移除 routes.go 中13条SQL审计路由 (API不再暴露, 返回404) - 移除 database.go AutoMigrate 中2个SQL审计表 - 移除 api.ts sqlAuditApi 对象 (37行) - 移除 mocks SQL审计mock数据 (90行) + barrel引用
This commit is contained in:
1 parent
a6e8b5ab4f
commit
a26f186e9b
8 files changed
+1
-1278
No files matched your search
@@ -1,435 +0,0 @@
|
|||||||
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,
|
|
||||||
TargetTable: 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) bool {
|
|
||||||
// 按 category 快速匹配
|
|
||||||
if rule.Category != "" {
|
|
||||||
if strings.Contains(sqlUpper, strings.ToUpper(rule.Category)) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// 按 pattern 正则匹配
|
|
||||||
if rule.Pattern != "" {
|
|
||||||
if re, err := regexp.Compile("(?i)" + rule.Pattern); err == nil {
|
|
||||||
if re.MatchString(sqlUpper) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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: "权限变更操作"},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -371,24 +371,6 @@ func Register(r *gin.Engine, db *gorm.DB, log *zap.Logger, cfg *config.Config) {
|
|||||||
audit.POST("/logs/clear", auditHandler.ClearAuditLogs)
|
audit.POST("/logs/clear", auditHandler.ClearAuditLogs)
|
||||||
}
|
}
|
||||||
|
|
||||||
// SQL审计路由 // Logan.Li
|
|
||||||
sqlAuditHandler := handlers.NewSQLAuditHandler(db)
|
|
||||||
sqlAudit := authorized.Group("/sql-audit")
|
|
||||||
{
|
|
||||||
sqlAudit.GET("/records", sqlAuditHandler.ListRecords)
|
|
||||||
sqlAudit.GET("/records/:id", sqlAuditHandler.GetRecord)
|
|
||||||
sqlAudit.POST("/analyze", sqlAuditHandler.AnalyzeSQL)
|
|
||||||
sqlAudit.POST("/records/:id/approve", sqlAuditHandler.ApproveRecord)
|
|
||||||
sqlAudit.POST("/records/:id/reject", sqlAuditHandler.RejectRecord)
|
|
||||||
sqlAudit.POST("/records/:id/execute", sqlAuditHandler.ExecuteRecord)
|
|
||||||
sqlAudit.DELETE("/records/:id", sqlAuditHandler.DeleteRecord)
|
|
||||||
sqlAudit.GET("/rules", sqlAuditHandler.ListRules)
|
|
||||||
sqlAudit.POST("/rules", sqlAuditHandler.CreateRule)
|
|
||||||
sqlAudit.PUT("/rules/:id", sqlAuditHandler.UpdateRule)
|
|
||||||
sqlAudit.DELETE("/rules/:id", sqlAuditHandler.DeleteRule)
|
|
||||||
sqlAudit.POST("/rules/:id/toggle", sqlAuditHandler.ToggleRule)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Agent 上报路由(无需 JWT)
|
// Agent 上报路由(无需 JWT)
|
||||||
agentHandler := handlers.NewAgentHandler(db)
|
agentHandler := handlers.NewAgentHandler(db)
|
||||||
taskHandler := handlers.NewAgentTaskHandler(db)
|
taskHandler := handlers.NewAgentTaskHandler(db)
|
||||||
|
|||||||
@@ -1,64 +0,0 @@
|
|||||||
package models
|
|
||||||
|
|
||||||
import (
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"gorm.io/gorm"
|
|
||||||
)
|
|
||||||
|
|
||||||
// SQLAuditRecord SQL审计记录 // Logan.Li
|
|
||||||
type SQLAuditRecord struct {
|
|
||||||
ID uint `gorm:"primarykey" json:"id"`
|
|
||||||
SQLContent string `gorm:"type:text;not null" json:"sql_content"`
|
|
||||||
SQLType string `gorm:"size:50" json:"sql_type"` // DDL/DML/DQL/DCL
|
|
||||||
DatabaseName string `gorm:"size:255" json:"database_name"`
|
|
||||||
TargetTable string `gorm:"column:table_name;size:255" json:"table_name"`
|
|
||||||
RiskLevel string `gorm:"size:20;default:safe" json:"risk_level"` // high/medium/low/safe
|
|
||||||
RiskRules string `gorm:"type:text" json:"risk_rules"` // JSON 数组
|
|
||||||
Status string `gorm:"size:20;default:pending" json:"status"` // pending/approved/rejected/executed
|
|
||||||
Executor string `gorm:"size:255" json:"executor"`
|
|
||||||
Approver string `gorm:"size:255" json:"approver"`
|
|
||||||
ExecuteTime *time.Time `json:"execute_time"`
|
|
||||||
AffectedRows int64 `gorm:"default:0" json:"affected_rows"`
|
|
||||||
ExecutionTime int64 `gorm:"default:0" json:"execution_time"` // 毫秒
|
|
||||||
SourceIP string `gorm:"size:100" json:"source_ip"`
|
|
||||||
Application string `gorm:"size:255" json:"application"`
|
|
||||||
TicketID string `gorm:"size:100" json:"ticket_id"`
|
|
||||||
CreatedAt time.Time `json:"created_at"`
|
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
|
||||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (SQLAuditRecord) TableName() string { return "sql_audit_records" }
|
|
||||||
|
|
||||||
// SQLAuditRule SQL审计规则 // Logan.Li
|
|
||||||
type SQLAuditRule struct {
|
|
||||||
ID uint `gorm:"primarykey" json:"id"`
|
|
||||||
Name string `gorm:"size:255;not null" json:"name"`
|
|
||||||
Description string `gorm:"type:text" json:"description"`
|
|
||||||
Pattern string `gorm:"type:text" json:"pattern"` // 正则/关键词匹配
|
|
||||||
Enabled bool `gorm:"default:true" json:"enabled"`
|
|
||||||
Severity string `gorm:"size:20;default:medium" json:"severity"` // high/medium/low
|
|
||||||
Category string `gorm:"size:100" json:"category"` // DDL/DML/DROP/TRUNCATE/etc
|
|
||||||
CreatedAt time.Time `json:"created_at"`
|
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
|
||||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (SQLAuditRule) TableName() string { return "sql_audit_rules" }
|
|
||||||
|
|
||||||
// AnalyzeSQLRequest SQL分析请求 // Logan.Li
|
|
||||||
type AnalyzeSQLRequest struct {
|
|
||||||
SQL string `json:"sql"`
|
|
||||||
DatabaseName string `json:"database_name"`
|
|
||||||
Application string `json:"application"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// AnalyzeSQLResponse SQL分析响应 // Logan.Li
|
|
||||||
type AnalyzeSQLResponse struct {
|
|
||||||
ID int64 `json:"id"`
|
|
||||||
RiskLevel string `json:"risk_level"`
|
|
||||||
RiskRules []string `json:"risk_rules"`
|
|
||||||
SQLType string `json:"sql_type"`
|
|
||||||
TableName string `json:"table_name"`
|
|
||||||
}
|
|
||||||
@@ -100,8 +100,6 @@ func AutoMigrate(db *gorm.DB) error {
|
|||||||
&models.AnsibleInventory{},
|
&models.AnsibleInventory{},
|
||||||
&models.LogSource{},
|
&models.LogSource{},
|
||||||
&models.LogEntry{},
|
&models.LogEntry{},
|
||||||
&models.SQLAuditRecord{},
|
|
||||||
&models.SQLAuditRule{},
|
|
||||||
&models.TopologyLayout{}, // 拓扑布局保存 // Logan.Li
|
&models.TopologyLayout{}, // 拓扑布局保存 // Logan.Li
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -1,629 +0,0 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
|
||||||
import {
|
|
||||||
Search, Filter, Eye, Download, Shield, AlertTriangle, CheckCircle, XCircle,
|
|
||||||
Database, FileText, Clock, User, Server, Code, AlertOctagon, RotateCcw,
|
|
||||||
ChevronDown, ChevronUp, Play, Pause, Settings, Trash2, RefreshCw, History
|
|
||||||
} from 'lucide-react';
|
|
||||||
import { Button } from '@/components/ui/button';
|
|
||||||
import { Input } from '@/components/ui/input';
|
|
||||||
import { Badge } from '@/components/ui/badge';
|
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
|
||||||
import {
|
|
||||||
Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter
|
|
||||||
} from '@/components/ui/dialog';
|
|
||||||
import {
|
|
||||||
Select, SelectContent, SelectItem, SelectTrigger, SelectValue
|
|
||||||
} from '@/components/ui/select';
|
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
|
||||||
import { Textarea } from '@/components/ui/textarea';
|
|
||||||
import { Switch } from '@/components/ui/switch';
|
|
||||||
import { Label } from '@/components/ui/label';
|
|
||||||
import { Separator } from '@/components/ui/separator';
|
|
||||||
import { toast } from 'sonner';
|
|
||||||
import { sqlAuditApi } from '@/services/api';
|
|
||||||
|
|
||||||
interface SQLAuditRecord {
|
|
||||||
id: string;
|
|
||||||
sqlContent: string;
|
|
||||||
sqlType: string;
|
|
||||||
databaseName: string;
|
|
||||||
tableName: string;
|
|
||||||
riskLevel: 'high' | 'medium' | 'low' | 'safe';
|
|
||||||
riskRules: string[];
|
|
||||||
status: 'pending' | 'approved' | 'rejected' | 'executed';
|
|
||||||
executor: string;
|
|
||||||
approver?: string;
|
|
||||||
executeTime?: string;
|
|
||||||
createdAt: string;
|
|
||||||
affectedRows?: number;
|
|
||||||
executionTime?: number;
|
|
||||||
sourceIP: string;
|
|
||||||
application: string;
|
|
||||||
ticketId?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface AuditRule {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
description: string;
|
|
||||||
enabled: boolean;
|
|
||||||
severity: 'high' | 'medium' | 'low';
|
|
||||||
category: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const mapRecord = (r: any): SQLAuditRecord => ({
|
|
||||||
id: String(r.id),
|
|
||||||
sqlContent: r.sql_content || '',
|
|
||||||
sqlType: r.sql_type || '',
|
|
||||||
databaseName: r.database_name || '',
|
|
||||||
tableName: r.table_name || '',
|
|
||||||
riskLevel: (r.risk_level || 'safe') as SQLAuditRecord['riskLevel'],
|
|
||||||
riskRules: (() => { try { return JSON.parse(r.risk_rules || '[]'); } catch { return []; } })(),
|
|
||||||
status: (r.status || 'pending') as SQLAuditRecord['status'],
|
|
||||||
executor: r.executor || '',
|
|
||||||
approver: r.approver || '',
|
|
||||||
executeTime: r.execute_time || undefined,
|
|
||||||
createdAt: r.created_at ? new Date(r.created_at).toLocaleString('zh-CN') : '',
|
|
||||||
affectedRows: r.affected_rows || 0,
|
|
||||||
executionTime: r.execution_time || undefined,
|
|
||||||
sourceIP: r.source_ip || '',
|
|
||||||
application: r.application || '',
|
|
||||||
ticketId: r.ticket_id || '',
|
|
||||||
});
|
|
||||||
|
|
||||||
const mapRule = (r: any): AuditRule => ({
|
|
||||||
id: String(r.id),
|
|
||||||
name: r.name || '',
|
|
||||||
description: r.description || '',
|
|
||||||
enabled: r.enabled ?? true,
|
|
||||||
severity: (r.severity || 'medium') as AuditRule['severity'],
|
|
||||||
category: r.category || '',
|
|
||||||
});
|
|
||||||
|
|
||||||
const SQLAuditManager: React.FC = () => {
|
|
||||||
const [records, setRecords] = useState<SQLAuditRecord[]>([]);
|
|
||||||
const [rules, setRules] = useState<AuditRule[]>([]);
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
const [searchQuery, setSearchQuery] = useState('');
|
|
||||||
const [riskFilter, setRiskFilter] = useState<string>('all');
|
|
||||||
const [statusFilter, setStatusFilter] = useState<string>('all');
|
|
||||||
const [selectedRecord, setSelectedRecord] = useState<SQLAuditRecord | null>(null);
|
|
||||||
const [detailOpen, setDetailOpen] = useState(false);
|
|
||||||
const [newAuditOpen, setNewAuditOpen] = useState(false);
|
|
||||||
const [rulesOpen, setRulesOpen] = useState(false);
|
|
||||||
const [sqlContent, setSqlContent] = useState('');
|
|
||||||
const [activeTab, setActiveTab] = useState('records');
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
fetchRecords();
|
|
||||||
fetchRules();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const fetchRecords = async () => {
|
|
||||||
setLoading(true);
|
|
||||||
try {
|
|
||||||
const res = await sqlAuditApi.getRecords();
|
|
||||||
setRecords((res.list || []).map(mapRecord));
|
|
||||||
} catch (error) {
|
|
||||||
toast.error('获取SQL审计记录失败');
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const fetchRules = async () => {
|
|
||||||
try {
|
|
||||||
const res = await sqlAuditApi.getRules();
|
|
||||||
setRules((res.list || []).map(mapRule));
|
|
||||||
} catch (error) {
|
|
||||||
toast.error('获取审计规则失败');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleAnalyzeSQL = async () => {
|
|
||||||
if (!sqlContent.trim()) {
|
|
||||||
toast.error('请输入SQL语句');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
await sqlAuditApi.analyzeSQL({ sql: sqlContent });
|
|
||||||
toast.success('SQL分析完成');
|
|
||||||
setNewAuditOpen(false);
|
|
||||||
setSqlContent('');
|
|
||||||
fetchRecords();
|
|
||||||
} catch (error) {
|
|
||||||
toast.error('SQL分析失败');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleApprove = async (id: string) => {
|
|
||||||
try {
|
|
||||||
await sqlAuditApi.approve(id);
|
|
||||||
toast.success('SQL已审批通过');
|
|
||||||
fetchRecords();
|
|
||||||
} catch (error) {
|
|
||||||
toast.error('审批失败');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleReject = async (id: string) => {
|
|
||||||
try {
|
|
||||||
await sqlAuditApi.reject(id);
|
|
||||||
toast.success('SQL已驳回');
|
|
||||||
fetchRecords();
|
|
||||||
} catch (error) {
|
|
||||||
toast.error('驳回失败');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleExecute = async (id: string) => {
|
|
||||||
try {
|
|
||||||
await sqlAuditApi.execute(id);
|
|
||||||
toast.success('SQL执行成功');
|
|
||||||
fetchRecords();
|
|
||||||
} catch (error) {
|
|
||||||
toast.error('SQL执行失败');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const toggleRule = async (ruleId: string) => {
|
|
||||||
try {
|
|
||||||
await sqlAuditApi.toggleRule(ruleId);
|
|
||||||
fetchRules();
|
|
||||||
toast.success('规则状态已更新');
|
|
||||||
} catch (error) {
|
|
||||||
toast.error('更新规则失败');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const getRiskBadge = (level: string) => {
|
|
||||||
switch (level) {
|
|
||||||
case 'high':
|
|
||||||
return <Badge variant="destructive" className="gap-1"><AlertOctagon className="w-3 h-3" />高危</Badge>;
|
|
||||||
case 'medium':
|
|
||||||
return <Badge variant="default" className="bg-orange-500 gap-1"><AlertTriangle className="w-3 h-3" />中危</Badge>;
|
|
||||||
case 'low':
|
|
||||||
return <Badge variant="default" className="bg-yellow-500 gap-1"><AlertTriangle className="w-3 h-3" />低危</Badge>;
|
|
||||||
case 'safe':
|
|
||||||
return <Badge variant="default" className="bg-green-500 gap-1"><CheckCircle className="w-3 h-3" />安全</Badge>;
|
|
||||||
default:
|
|
||||||
return <Badge variant="secondary">未知</Badge>;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const getStatusBadge = (status: string) => {
|
|
||||||
switch (status) {
|
|
||||||
case 'pending':
|
|
||||||
return <Badge variant="outline" className="gap-1"><Clock className="w-3 h-3" />待审批</Badge>;
|
|
||||||
case 'approved':
|
|
||||||
return <Badge variant="default" className="bg-blue-500 gap-1"><CheckCircle className="w-3 h-3" />已批准</Badge>;
|
|
||||||
case 'rejected':
|
|
||||||
return <Badge variant="destructive" className="gap-1"><XCircle className="w-3 h-3" />已驳回</Badge>;
|
|
||||||
case 'executed':
|
|
||||||
return <Badge variant="default" className="bg-green-500 gap-1"><CheckCircle className="w-3 h-3" />已执行</Badge>;
|
|
||||||
default:
|
|
||||||
return <Badge variant="secondary">未知</Badge>;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const filteredRecords = records.filter(record => {
|
|
||||||
const matchSearch =
|
|
||||||
record.sqlContent.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
|
||||||
record.databaseName.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
|
||||||
record.executor.toLowerCase().includes(searchQuery.toLowerCase());
|
|
||||||
const matchRisk = riskFilter === 'all' || record.riskLevel === riskFilter;
|
|
||||||
const matchStatus = statusFilter === 'all' || record.status === statusFilter;
|
|
||||||
return matchSearch && matchRisk && matchStatus;
|
|
||||||
});
|
|
||||||
|
|
||||||
const stats = {
|
|
||||||
total: records.length,
|
|
||||||
highRisk: records.filter(r => r.riskLevel === 'high').length,
|
|
||||||
pending: records.filter(r => r.status === 'pending').length,
|
|
||||||
executed: records.filter(r => r.status === 'executed').length
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="space-y-6">
|
|
||||||
{/* 统计卡片 */}
|
|
||||||
<div className="grid grid-cols-4 gap-4">
|
|
||||||
<Card>
|
|
||||||
<CardContent className="p-4">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div>
|
|
||||||
<p className="text-sm text-muted-foreground">总审计数</p>
|
|
||||||
<p className="text-2xl font-bold">{stats.total}</p>
|
|
||||||
</div>
|
|
||||||
<div className="p-3 bg-blue-100 rounded-lg">
|
|
||||||
<Database className="w-5 h-5 text-blue-600" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
<Card>
|
|
||||||
<CardContent className="p-4">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div>
|
|
||||||
<p className="text-sm text-muted-foreground">高危SQL</p>
|
|
||||||
<p className="text-2xl font-bold text-red-600">{stats.highRisk}</p>
|
|
||||||
</div>
|
|
||||||
<div className="p-3 bg-red-100 rounded-lg">
|
|
||||||
<AlertOctagon className="w-5 h-5 text-red-600" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
<Card>
|
|
||||||
<CardContent className="p-4">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div>
|
|
||||||
<p className="text-sm text-muted-foreground">待审批</p>
|
|
||||||
<p className="text-2xl font-bold text-orange-600">{stats.pending}</p>
|
|
||||||
</div>
|
|
||||||
<div className="p-3 bg-orange-100 rounded-lg">
|
|
||||||
<Clock className="w-5 h-5 text-orange-600" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
<Card>
|
|
||||||
<CardContent className="p-4">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div>
|
|
||||||
<p className="text-sm text-muted-foreground">已执行</p>
|
|
||||||
<p className="text-2xl font-bold text-green-600">{stats.executed}</p>
|
|
||||||
</div>
|
|
||||||
<div className="p-3 bg-green-100 rounded-lg">
|
|
||||||
<CheckCircle className="w-5 h-5 text-green-600" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Tabs value={activeTab} onValueChange={setActiveTab}>
|
|
||||||
<TabsList className="grid w-full grid-cols-2 lg:w-[400px]">
|
|
||||||
<TabsTrigger value="records">审计记录</TabsTrigger>
|
|
||||||
<TabsTrigger value="rules">审计规则</TabsTrigger>
|
|
||||||
</TabsList>
|
|
||||||
|
|
||||||
<TabsContent value="records" className="space-y-4">
|
|
||||||
{/* 操作栏 */}
|
|
||||||
<div className="flex flex-wrap items-center justify-between gap-4">
|
|
||||||
<div className="flex items-center gap-2 flex-1 min-w-[300px]">
|
|
||||||
<div className="relative flex-1">
|
|
||||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" />
|
|
||||||
<Input
|
|
||||||
placeholder="搜索SQL内容、数据库、执行人..."
|
|
||||||
value={searchQuery}
|
|
||||||
onChange={(e) => setSearchQuery(e.target.value)}
|
|
||||||
className="pl-10"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<Select value={riskFilter} onValueChange={setRiskFilter}>
|
|
||||||
<SelectTrigger className="w-[120px]">
|
|
||||||
<SelectValue placeholder="风险等级" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="all">全部风险</SelectItem>
|
|
||||||
<SelectItem value="high">高危</SelectItem>
|
|
||||||
<SelectItem value="medium">中危</SelectItem>
|
|
||||||
<SelectItem value="low">低危</SelectItem>
|
|
||||||
<SelectItem value="safe">安全</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
<Select value={statusFilter} onValueChange={setStatusFilter}>
|
|
||||||
<SelectTrigger className="w-[120px]">
|
|
||||||
<SelectValue placeholder="状态" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="all">全部状态</SelectItem>
|
|
||||||
<SelectItem value="pending">待审批</SelectItem>
|
|
||||||
<SelectItem value="approved">已批准</SelectItem>
|
|
||||||
<SelectItem value="rejected">已驳回</SelectItem>
|
|
||||||
<SelectItem value="executed">已执行</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Button variant="outline" onClick={fetchRecords}>
|
|
||||||
<RefreshCw className="w-4 h-4 mr-2" />
|
|
||||||
刷新
|
|
||||||
</Button>
|
|
||||||
<Button onClick={() => setNewAuditOpen(true)}>
|
|
||||||
<Shield className="w-4 h-4 mr-2" />
|
|
||||||
新建审计
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 记录列表 */}
|
|
||||||
<Card>
|
|
||||||
<CardContent className="p-0">
|
|
||||||
<div className="overflow-x-auto">
|
|
||||||
<table className="w-full">
|
|
||||||
<thead className="bg-muted/50">
|
|
||||||
<tr>
|
|
||||||
<th className="px-4 py-3 text-left text-sm font-medium">SQL内容</th>
|
|
||||||
<th className="px-4 py-3 text-left text-sm font-medium">数据库</th>
|
|
||||||
<th className="px-4 py-3 text-left text-sm font-medium">风险等级</th>
|
|
||||||
<th className="px-4 py-3 text-left text-sm font-medium">状态</th>
|
|
||||||
<th className="px-4 py-3 text-left text-sm font-medium">执行人</th>
|
|
||||||
<th className="px-4 py-3 text-left text-sm font-medium">创建时间</th>
|
|
||||||
<th className="px-4 py-3 text-left text-sm font-medium">操作</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody className="divide-y">
|
|
||||||
{loading ? (
|
|
||||||
<tr>
|
|
||||||
<td colSpan={7} className="px-4 py-8 text-center text-muted-foreground">
|
|
||||||
加载中...
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
) : filteredRecords.length === 0 ? (
|
|
||||||
<tr>
|
|
||||||
<td colSpan={7} className="px-4 py-8 text-center text-muted-foreground">
|
|
||||||
暂无SQL审计记录
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
) : (
|
|
||||||
filteredRecords.map((record) => (
|
|
||||||
<tr key={record.id} className="hover:bg-muted/50">
|
|
||||||
<td className="px-4 py-3">
|
|
||||||
<div className="max-w-[300px] truncate font-mono text-sm" title={record.sqlContent}>
|
|
||||||
{record.sqlContent}
|
|
||||||
</div>
|
|
||||||
<div className="flex gap-1 mt-1">
|
|
||||||
{record.riskRules.map((rule, idx) => (
|
|
||||||
<Badge key={idx} variant="outline" className="text-xs">{rule}</Badge>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td className="px-4 py-3 text-sm">
|
|
||||||
<div>{record.databaseName}</div>
|
|
||||||
<div className="text-muted-foreground text-xs">{record.tableName}</div>
|
|
||||||
</td>
|
|
||||||
<td className="px-4 py-3">{getRiskBadge(record.riskLevel)}</td>
|
|
||||||
<td className="px-4 py-3">{getStatusBadge(record.status)}</td>
|
|
||||||
<td className="px-4 py-3 text-sm">{record.executor}</td>
|
|
||||||
<td className="px-4 py-3 text-sm text-muted-foreground">{record.createdAt}</td>
|
|
||||||
<td className="px-4 py-3">
|
|
||||||
<div className="flex items-center gap-1">
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => { setSelectedRecord(record); setDetailOpen(true); }}
|
|
||||||
>
|
|
||||||
<Eye className="w-4 h-4" />
|
|
||||||
</Button>
|
|
||||||
{record.status === 'pending' && (
|
|
||||||
<>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => handleApprove(record.id)}
|
|
||||||
>
|
|
||||||
<CheckCircle className="w-4 h-4 text-green-600" />
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => handleReject(record.id)}
|
|
||||||
>
|
|
||||||
<XCircle className="w-4 h-4 text-red-600" />
|
|
||||||
</Button>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{record.status === 'approved' && (
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => handleExecute(record.id)}
|
|
||||||
>
|
|
||||||
<Play className="w-4 h-4 text-blue-600" />
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</TabsContent>
|
|
||||||
|
|
||||||
<TabsContent value="rules" className="space-y-4">
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle className="flex items-center gap-2">
|
|
||||||
<Shield className="w-5 h-5" />
|
|
||||||
SQL审计规则配置
|
|
||||||
</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<div className="space-y-4">
|
|
||||||
{rules.map((rule) => (
|
|
||||||
<div key={rule.id} className="flex items-center justify-between p-4 border rounded-lg">
|
|
||||||
<div className="flex-1">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<span className="font-medium">{rule.name}</span>
|
|
||||||
<Badge variant={rule.severity === 'high' ? 'destructive' : rule.severity === 'medium' ? 'default' : 'secondary'}>
|
|
||||||
{rule.severity === 'high' ? '高危' : rule.severity === 'medium' ? '中危' : '低危'}
|
|
||||||
</Badge>
|
|
||||||
<Badge variant="outline">{rule.category}</Badge>
|
|
||||||
</div>
|
|
||||||
<p className="text-sm text-muted-foreground mt-1">{rule.description}</p>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-4">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Switch
|
|
||||||
checked={rule.enabled}
|
|
||||||
onCheckedChange={() => toggleRule(rule.id)}
|
|
||||||
/>
|
|
||||||
<Label className="text-sm">{rule.enabled ? '已启用' : '已禁用'}</Label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</TabsContent>
|
|
||||||
</Tabs>
|
|
||||||
|
|
||||||
{/* 新建审计对话框 */}
|
|
||||||
<Dialog open={newAuditOpen} onOpenChange={setNewAuditOpen}>
|
|
||||||
<DialogContent className="max-w-3xl">
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle>新建SQL审计</DialogTitle>
|
|
||||||
<DialogDescription>输入SQL语句进行风险分析和审计</DialogDescription>
|
|
||||||
</DialogHeader>
|
|
||||||
<div className="space-y-4">
|
|
||||||
<div>
|
|
||||||
<Label>SQL语句</Label>
|
|
||||||
<Textarea
|
|
||||||
value={sqlContent}
|
|
||||||
onChange={(e) => setSqlContent(e.target.value)}
|
|
||||||
placeholder="请输入需要审计的SQL语句..."
|
|
||||||
className="min-h-[200px] font-mono text-sm"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
|
||||||
<div>
|
|
||||||
<Label>目标数据库</Label>
|
|
||||||
<Select>
|
|
||||||
<SelectTrigger>
|
|
||||||
<SelectValue placeholder="选择数据库" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="db1">生产数据库</SelectItem>
|
|
||||||
<SelectItem value="db2">测试数据库</SelectItem>
|
|
||||||
<SelectItem value="db3">开发数据库</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<Label>执行环境</Label>
|
|
||||||
<Select>
|
|
||||||
<SelectTrigger>
|
|
||||||
<SelectValue placeholder="选择环境" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="prod">生产环境</SelectItem>
|
|
||||||
<SelectItem value="staging">预发环境</SelectItem>
|
|
||||||
<SelectItem value="dev">开发环境</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<DialogFooter>
|
|
||||||
<Button variant="outline" onClick={() => setNewAuditOpen(false)}>取消</Button>
|
|
||||||
<Button onClick={handleAnalyzeSQL}>
|
|
||||||
<Shield className="w-4 h-4 mr-2" />
|
|
||||||
分析SQL
|
|
||||||
</Button>
|
|
||||||
</DialogFooter>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
|
|
||||||
{/* 详情对话框 */}
|
|
||||||
<Dialog open={detailOpen} onOpenChange={setDetailOpen}>
|
|
||||||
<DialogContent className="max-w-3xl">
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle>SQL审计详情</DialogTitle>
|
|
||||||
<DialogDescription className="sr-only">查看SQL审计记录详情</DialogDescription>
|
|
||||||
</DialogHeader>
|
|
||||||
{selectedRecord && (
|
|
||||||
<div className="space-y-4">
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
|
||||||
<div>
|
|
||||||
<Label className="text-muted-foreground">数据库</Label>
|
|
||||||
<p className="font-medium">{selectedRecord.databaseName}</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<Label className="text-muted-foreground">数据表</Label>
|
|
||||||
<p className="font-medium">{selectedRecord.tableName}</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<Label className="text-muted-foreground">SQL类型</Label>
|
|
||||||
<p className="font-medium">{selectedRecord.sqlType}</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<Label className="text-muted-foreground">风险等级</Label>
|
|
||||||
<p>{getRiskBadge(selectedRecord.riskLevel)}</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<Label className="text-muted-foreground">状态</Label>
|
|
||||||
<p>{getStatusBadge(selectedRecord.status)}</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<Label className="text-muted-foreground">执行人</Label>
|
|
||||||
<p className="font-medium">{selectedRecord.executor}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<Label className="text-muted-foreground">SQL内容</Label>
|
|
||||||
<div className="p-3 bg-muted rounded-lg font-mono text-sm mt-1">
|
|
||||||
<pre>{selectedRecord.sqlContent}</pre>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{selectedRecord.riskRules.length > 0 && (
|
|
||||||
<div>
|
|
||||||
<Label className="text-muted-foreground">触发规则</Label>
|
|
||||||
<div className="flex gap-2 mt-1">
|
|
||||||
{selectedRecord.riskRules.map((rule, idx) => (
|
|
||||||
<Badge key={idx} variant="destructive">{rule}</Badge>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{selectedRecord.executionTime && (
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
|
||||||
<div>
|
|
||||||
<Label className="text-muted-foreground">执行时间</Label>
|
|
||||||
<p className="font-medium">{selectedRecord.executeTime}</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<Label className="text-muted-foreground">执行耗时</Label>
|
|
||||||
<p className="font-medium">{selectedRecord.executionTime}ms</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<Label className="text-muted-foreground">影响行数</Label>
|
|
||||||
<p className="font-medium">{selectedRecord.affectedRows}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<DialogFooter>
|
|
||||||
<Button variant="outline" onClick={() => setDetailOpen(false)}>关闭</Button>
|
|
||||||
{selectedRecord?.status === 'pending' && (
|
|
||||||
<>
|
|
||||||
<Button variant="destructive" onClick={() => { handleReject(selectedRecord.id); setDetailOpen(false); }}>
|
|
||||||
<XCircle className="w-4 h-4 mr-2" />
|
|
||||||
驳回
|
|
||||||
</Button>
|
|
||||||
<Button onClick={() => { handleApprove(selectedRecord.id); setDetailOpen(false); }}>
|
|
||||||
<CheckCircle className="w-4 h-4 mr-2" />
|
|
||||||
批准
|
|
||||||
</Button>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{selectedRecord?.status === 'approved' && (
|
|
||||||
<Button onClick={() => { handleExecute(selectedRecord.id); setDetailOpen(false); }}>
|
|
||||||
<Play className="w-4 h-4 mr-2" />
|
|
||||||
执行SQL
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</DialogFooter>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default SQLAuditManager;
|
|
||||||
@@ -12,7 +12,7 @@ export * from './modules/monitor';
|
|||||||
export * from './modules/misc';
|
export * from './modules/misc';
|
||||||
|
|
||||||
// Aggregate mockData object (cross-module references)
|
// Aggregate mockData object (cross-module references)
|
||||||
import { sqlAuditRecords, sqlAuditRules, resourceModels, modelInstances } from './modules/misc';
|
import { resourceModels, modelInstances } from './modules/misc';
|
||||||
import { ansiblePlaybooks, ansibleJobs, ansibleInventories, cronJobsList, cronJobExecutions, scripts } from './modules/automation';
|
import { ansiblePlaybooks, ansibleJobs, ansibleInventories, cronJobsList, cronJobExecutions, scripts } from './modules/automation';
|
||||||
import { logMonitorEntries, logSources, traces, serviceMetrics, traceSpans, grafanaDashboards, datasources } from './modules/monitor';
|
import { logMonitorEntries, logSources, traces, serviceMetrics, traceSpans, grafanaDashboards, datasources } from './modules/monitor';
|
||||||
import { alertCenterAlerts, alertCenterRules, notificationChannels, recentAlerts, alertEvents } from './modules/alerts';
|
import { alertCenterAlerts, alertCenterRules, notificationChannels, recentAlerts, alertEvents } from './modules/alerts';
|
||||||
@@ -25,8 +25,6 @@ import { pendingApprovals, currentUser, users } from './modules/system';
|
|||||||
import { tickets, approvalFlows, approvalInstances } from './modules/approval';
|
import { tickets, approvalFlows, approvalInstances } from './modules/approval';
|
||||||
|
|
||||||
export const mockData = {
|
export const mockData = {
|
||||||
sqlAuditRecords,
|
|
||||||
sqlAuditRules,
|
|
||||||
resourceModels,
|
resourceModels,
|
||||||
modelInstances,
|
modelInstances,
|
||||||
ansiblePlaybooks,
|
ansiblePlaybooks,
|
||||||
|
|||||||
@@ -132,96 +132,6 @@ export const modelInstances: ModelInstance[] = [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
// ============ SQL审计数据 ============
|
|
||||||
export const sqlAuditRecords = [
|
|
||||||
{
|
|
||||||
id: 'audit-001',
|
|
||||||
sqlContent: 'UPDATE users SET status = "inactive" WHERE last_login < "2024-01-01"',
|
|
||||||
sqlType: 'UPDATE',
|
|
||||||
databaseName: 'production_db',
|
|
||||||
tableName: 'users',
|
|
||||||
riskLevel: 'medium',
|
|
||||||
riskRules: ['批量更新', '无WHERE条件限制'],
|
|
||||||
status: 'pending',
|
|
||||||
executor: '张工程师',
|
|
||||||
createdAt: '2025-04-14 10:30:00',
|
|
||||||
sourceIP: '192.168.1.100',
|
|
||||||
application: 'user-management-service'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'audit-002',
|
|
||||||
sqlContent: 'DELETE FROM logs WHERE created_at < DATE_SUB(NOW(), INTERVAL 90 DAY)',
|
|
||||||
sqlType: 'DELETE',
|
|
||||||
databaseName: 'production_db',
|
|
||||||
tableName: 'logs',
|
|
||||||
riskLevel: 'high',
|
|
||||||
riskRules: ['批量删除', '影响大量数据'],
|
|
||||||
status: 'approved',
|
|
||||||
executor: '李运维',
|
|
||||||
approver: '王经理',
|
|
||||||
executeTime: '2025-04-14 09:00:00',
|
|
||||||
createdAt: '2025-04-14 08:30:00',
|
|
||||||
affectedRows: 500000,
|
|
||||||
executionTime: 12500,
|
|
||||||
sourceIP: '192.168.1.101',
|
|
||||||
application: 'log-cleanup-service'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'audit-003',
|
|
||||||
sqlContent: 'SELECT * FROM orders WHERE user_id = 12345',
|
|
||||||
sqlType: 'SELECT',
|
|
||||||
databaseName: 'production_db',
|
|
||||||
tableName: 'orders',
|
|
||||||
riskLevel: 'safe',
|
|
||||||
riskRules: [],
|
|
||||||
status: 'executed',
|
|
||||||
executor: '王开发',
|
|
||||||
executeTime: '2025-04-14 10:15:00',
|
|
||||||
createdAt: '2025-04-14 10:15:00',
|
|
||||||
affectedRows: 0,
|
|
||||||
executionTime: 45,
|
|
||||||
sourceIP: '192.168.1.102',
|
|
||||||
application: 'order-service'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'audit-004',
|
|
||||||
sqlContent: 'ALTER TABLE users ADD COLUMN phone_verified BOOLEAN DEFAULT FALSE',
|
|
||||||
sqlType: 'DDL',
|
|
||||||
databaseName: 'production_db',
|
|
||||||
tableName: 'users',
|
|
||||||
riskLevel: 'high',
|
|
||||||
riskRules: ['DDL操作', '表结构变更'],
|
|
||||||
status: 'pending',
|
|
||||||
executor: '张工程师',
|
|
||||||
createdAt: '2025-04-14 11:00:00',
|
|
||||||
sourceIP: '192.168.1.100',
|
|
||||||
application: 'db-migration-tool'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'audit-005',
|
|
||||||
sqlContent: 'DROP TABLE temp_backup_2024',
|
|
||||||
sqlType: 'DDL',
|
|
||||||
databaseName: 'production_db',
|
|
||||||
tableName: 'temp_backup_2024',
|
|
||||||
riskLevel: 'high',
|
|
||||||
riskRules: ['删除表', '高危操作'],
|
|
||||||
status: 'rejected',
|
|
||||||
executor: '李运维',
|
|
||||||
createdAt: '2025-04-14 09:30:00',
|
|
||||||
sourceIP: '192.168.1.101',
|
|
||||||
application: 'cleanup-script'
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
export const sqlAuditRules = [
|
|
||||||
{ id: 'rule-001', name: '禁止无WHERE条件的UPDATE', description: '检测UPDATE语句是否包含WHERE条件', enabled: true, severity: 'high', category: 'DML' },
|
|
||||||
{ id: 'rule-002', name: '禁止无WHERE条件的DELETE', description: '检测DELETE语句是否包含WHERE条件', enabled: true, severity: 'high', category: 'DML' },
|
|
||||||
{ id: 'rule-003', name: '限制批量更新影响行数', description: 'UPDATE/DELETE影响行数超过1000条需要审批', enabled: true, severity: 'medium', category: 'DML' },
|
|
||||||
{ id: 'rule-004', name: 'DDL操作需要审批', description: 'ALTER、DROP等DDL操作需要高级审批', enabled: true, severity: 'high', category: 'DDL' },
|
|
||||||
{ id: 'rule-005', name: '禁止SELECT *', description: '建议明确指定查询字段', enabled: false, severity: 'low', category: 'DQL' },
|
|
||||||
{ id: 'rule-006', name: '敏感表操作审计', description: '对用户表、订单表等敏感表的操作进行审计', enabled: true, severity: 'medium', category: 'AUDIT' }
|
|
||||||
];
|
|
||||||
|
|
||||||
// ============ Ansible数据 ============
|
// ============ Ansible数据 ============
|
||||||
// mockData aggregate moved to data.ts barrel (references cross-module exports)
|
// mockData aggregate moved to data.ts barrel (references cross-module exports)
|
||||||
|
|
||||||
@@ -527,43 +527,6 @@ export const auditApi = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// ==================== SQL 审计 API ====================
|
|
||||||
export const sqlAuditApi = {
|
|
||||||
getRecords: async (params?: Record<string, string>) => {
|
|
||||||
return http.get<ListResponse>('/sql-audit/records', params);
|
|
||||||
},
|
|
||||||
getRules: async () => {
|
|
||||||
return http.get<ListResponse>('/sql-audit/rules');
|
|
||||||
},
|
|
||||||
analyzeSQL: async (data: { sql: string; database_name?: string; application?: string }) => {
|
|
||||||
return http.post('/sql-audit/analyze', data);
|
|
||||||
},
|
|
||||||
approve: async (id: string | number) => {
|
|
||||||
return http.post(`/sql-audit/records/${id}/approve`);
|
|
||||||
},
|
|
||||||
reject: async (id: string | number) => {
|
|
||||||
return http.post(`/sql-audit/records/${id}/reject`);
|
|
||||||
},
|
|
||||||
execute: async (id: string | number) => {
|
|
||||||
return http.post(`/sql-audit/records/${id}/execute`);
|
|
||||||
},
|
|
||||||
deleteRecord: async (id: string | number) => {
|
|
||||||
return http.delete(`/sql-audit/records/${id}`);
|
|
||||||
},
|
|
||||||
createRule: async (data: any) => {
|
|
||||||
return http.post('/sql-audit/rules', data);
|
|
||||||
},
|
|
||||||
updateRule: async (id: string | number, data: any) => {
|
|
||||||
return http.put(`/sql-audit/rules/${id}`, data);
|
|
||||||
},
|
|
||||||
deleteRule: async (id: string | number) => {
|
|
||||||
return http.delete(`/sql-audit/rules/${id}`);
|
|
||||||
},
|
|
||||||
toggleRule: async (ruleId: string | number) => {
|
|
||||||
return http.post(`/sql-audit/rules/${ruleId}/toggle`);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
// ==================== Ansible 自动化 API ====================
|
// ==================== Ansible 自动化 API ====================
|
||||||
export const ansibleApi = {
|
export const ansibleApi = {
|
||||||
// Playbook 管理
|
// Playbook 管理
|
||||||
|
|||||||
Reference in new issue
Block a user