- Agent CLI: 多轮对话、SSH 长连接、流式推理、Evolver 按用户隔离、历史侧栏 - 终端审计: 回放播放器、命令书签、筛选分页导出、命令输出统计、录制落盘修复 - 修复 session_commands 表迁移、Kill 竞态导致录制丢失、无录制会话回放提示 - 审计历史页只读(移除删除),移除回放弹框录制搜索
418 lines
12 KiB
Go
418 lines
12 KiB
Go
package controller
|
||
|
||
import (
|
||
"bytes"
|
||
"encoding/csv"
|
||
"fmt"
|
||
"net/http"
|
||
"strconv"
|
||
"time"
|
||
|
||
"ezops/model"
|
||
|
||
"github.com/gin-gonic/gin"
|
||
"gorm.io/gorm"
|
||
)
|
||
|
||
// AuditHandler 审计日志处理器
|
||
type AuditHandler struct {
|
||
db *gorm.DB
|
||
}
|
||
|
||
// NewAuditHandler 创建审计日志处理器
|
||
func NewAuditHandler(db *gorm.DB) *AuditHandler {
|
||
return &AuditHandler{db: db}
|
||
}
|
||
|
||
// ListAuditLogs 获取审计日志列表
|
||
// @Summary 获取审计日志列表
|
||
// @Description 获取审计日志列表,支持分页和筛选
|
||
// @Tags 审计日志
|
||
// @Accept json
|
||
// @Produce json
|
||
// @Param page query int false "页码" default(1)
|
||
// @Param pageSize query int false "每页数量" default(20)
|
||
// @Param module query string false "模块"
|
||
// @Param action query string false "操作类型"
|
||
// @Param username query string false "用户名"
|
||
// @Param startTime query string false "开始时间"
|
||
// @Param endTime query string false "结束时间"
|
||
// @Success 200 {object} model.AuditLogListResponse
|
||
// @Router /api/v1/audit/logs [get]
|
||
func (h *AuditHandler) ListAuditLogs(c *gin.Context) {
|
||
var req model.AuditLogQuery
|
||
if err := c.ShouldBindQuery(&req); err != nil {
|
||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: err.Error()})
|
||
return
|
||
}
|
||
|
||
// 设置默认值
|
||
if req.Page <= 0 {
|
||
req.Page = 1
|
||
}
|
||
if req.PageSize <= 0 {
|
||
req.PageSize = 20
|
||
}
|
||
|
||
// 构建查询
|
||
query := h.db.Model(&model.AuditLog{})
|
||
|
||
if req.Module != "" {
|
||
query = query.Where("module = ?", req.Module)
|
||
}
|
||
if req.Action != "" {
|
||
query = query.Where("action = ?", req.Action)
|
||
}
|
||
if req.Username != "" {
|
||
query = query.Where("username LIKE ?", "%"+req.Username+"%")
|
||
}
|
||
if req.StartTime != "" {
|
||
query = query.Where("created_at >= ?", req.StartTime)
|
||
}
|
||
if req.EndTime != "" {
|
||
query = query.Where("created_at <= ?", req.EndTime)
|
||
}
|
||
|
||
// 获取总数
|
||
var total int64
|
||
if err := query.Count(&total).Error; err != nil {
|
||
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "查询失败"})
|
||
return
|
||
}
|
||
|
||
// 获取列表
|
||
var logs []model.AuditLog
|
||
if err := query.Order("created_at DESC").
|
||
Offset((req.Page - 1) * req.PageSize).
|
||
Limit(req.PageSize).
|
||
Find(&logs).Error; err != nil {
|
||
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "查询失败"})
|
||
return
|
||
}
|
||
|
||
c.JSON(http.StatusOK, model.AuditLogListResponse{
|
||
List: logs,
|
||
Total: total,
|
||
})
|
||
}
|
||
|
||
// GetAuditLogDetail 获取审计日志详情
|
||
// @Summary 获取审计日志详情
|
||
// @Description 获取指定ID的审计日志详情
|
||
// @Tags 审计日志
|
||
// @Accept json
|
||
// @Produce json
|
||
// @Param id path int true "日志ID"
|
||
// @Success 200 {object} model.AuditLog
|
||
// @Router /api/v1/audit/logs/{id} [get]
|
||
func (h *AuditHandler) GetAuditLogDetail(c *gin.Context) {
|
||
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
||
if err != nil {
|
||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "无效的日志ID"})
|
||
return
|
||
}
|
||
|
||
var log model.AuditLog
|
||
if err := h.db.First(&log, id).Error; err != nil {
|
||
if err == gorm.ErrRecordNotFound {
|
||
c.JSON(http.StatusNotFound, model.Response{Code: 404, Message: "日志不存在"})
|
||
return
|
||
}
|
||
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "查询失败"})
|
||
return
|
||
}
|
||
|
||
c.JSON(http.StatusOK, log)
|
||
}
|
||
|
||
// GetAuditModules 获取审计模块列表
|
||
// @Summary 获取审计模块列表
|
||
// @Description 获取所有审计模块类型
|
||
// @Tags 审计日志
|
||
// @Accept json
|
||
// @Produce json
|
||
// @Success 200 {object} map[string]interface{}
|
||
// @Router /api/v1/audit/modules [get]
|
||
func (h *AuditHandler) GetAuditModules(c *gin.Context) {
|
||
modules := []map[string]string{
|
||
{"value": "user", "label": "用户管理"},
|
||
{"value": "server", "label": "服务器管理"},
|
||
{"value": "kubernetes", "label": "Kubernetes"},
|
||
{"value": "alert", "label": "监控告警"},
|
||
{"value": "pipeline", "label": "应用发布"},
|
||
{"value": "automation", "label": "自动化运维"},
|
||
{"value": "approval", "label": "审批流程"},
|
||
{"value": "navigation", "label": "站内导航"},
|
||
{"value": "cloud_account", "label": "云账号管理"},
|
||
{"value": "domain", "label": "域名管理"},
|
||
{"value": "agent", "label": "Agent管理"},
|
||
}
|
||
|
||
c.JSON(http.StatusOK, model.Response{Code: 200, Data: modules}) // Logan.Li
|
||
}
|
||
|
||
// GetAuditActions 获取审计操作类型列表
|
||
// @Summary 获取审计操作类型列表
|
||
// @Description 获取所有审计操作类型
|
||
// @Tags 审计日志
|
||
// @Accept json
|
||
// @Produce json
|
||
// @Success 200 {object} map[string]interface{}
|
||
// @Router /api/v1/audit/actions [get]
|
||
func (h *AuditHandler) GetAuditActions(c *gin.Context) {
|
||
actions := []map[string]string{
|
||
{"value": "create", "label": "创建"},
|
||
{"value": "update", "label": "更新"},
|
||
{"value": "delete", "label": "删除"},
|
||
{"value": "view", "label": "查看"},
|
||
{"value": "login", "label": "登录"},
|
||
{"value": "logout", "label": "登出"},
|
||
{"value": "execute", "label": "执行"},
|
||
{"value": "approve", "label": "审批"},
|
||
{"value": "reject", "label": "驳回"},
|
||
{"value": "import", "label": "导入"},
|
||
{"value": "export", "label": "导出"},
|
||
}
|
||
|
||
c.JSON(http.StatusOK, model.Response{Code: 200, Data: actions}) // Logan.Li
|
||
}
|
||
|
||
// buildAuditQuery 构建审计日志筛选查询(复用于列表和导出)
|
||
func (h *AuditHandler) buildAuditQuery(req model.AuditLogQuery) *gorm.DB {
|
||
query := h.db.Model(&model.AuditLog{})
|
||
if req.Module != "" {
|
||
query = query.Where("module = ?", req.Module)
|
||
}
|
||
if req.Action != "" {
|
||
query = query.Where("action = ?", req.Action)
|
||
}
|
||
if req.Username != "" {
|
||
query = query.Where("username LIKE ?", "%"+req.Username+"%")
|
||
}
|
||
if req.StartTime != "" {
|
||
query = query.Where("created_at >= ?", req.StartTime)
|
||
}
|
||
if req.EndTime != "" {
|
||
query = query.Where("created_at <= ?", req.EndTime)
|
||
}
|
||
return query
|
||
}
|
||
|
||
// ExportAuditLogs 导出审计日志
|
||
// @Summary 导出审计日志
|
||
// @Description 导出审计日志为CSV格式(带UTF-8 BOM,Excel可直接打开)
|
||
// @Tags 审计日志
|
||
// @Accept json
|
||
// @Produce text/csv
|
||
// @Param module query string false "模块"
|
||
// @Param action query string false "操作类型"
|
||
// @Param username query string false "用户名"
|
||
// @Param startTime query string false "开始时间"
|
||
// @Param endTime query string false "结束时间"
|
||
// @Param format query string false "导出格式" default("csv") Enums(csv, json)
|
||
// @Success 200 {file} binary
|
||
// @Router /api/v1/audit/logs/export [get]
|
||
func (h *AuditHandler) ExportAuditLogs(c *gin.Context) {
|
||
var req model.AuditLogQuery
|
||
_ = c.ShouldBindQuery(&req)
|
||
|
||
// 导出最多 10000 条,防止内存溢出
|
||
query := h.buildAuditQuery(req)
|
||
|
||
var logs []model.AuditLog
|
||
if err := query.Order("created_at DESC").Limit(10000).Find(&logs).Error; err != nil {
|
||
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "导出失败: " + err.Error()})
|
||
return
|
||
}
|
||
|
||
format := c.DefaultQuery("format", "csv")
|
||
|
||
switch format {
|
||
case "json":
|
||
// JSON 格式(向后兼容)
|
||
c.JSON(http.StatusOK, model.Response{Code: 200, Message: "导出成功", Data: gin.H{
|
||
"count": len(logs),
|
||
"logs": logs,
|
||
}}) // Logan.Li
|
||
return
|
||
|
||
case "csv":
|
||
fallthrough
|
||
|
||
default:
|
||
// CSV 格式 — 带 UTF-8 BOM 以便 Excel 正确显示中文
|
||
var buf bytes.Buffer
|
||
buf.WriteRune('\uFEFF') // UTF-8 BOM
|
||
|
||
writer := csv.NewWriter(&buf)
|
||
_ = writer.Write([]string{"ID", "时间", "用户", "IP地址", "模块", "操作类型", "资源", "资源ID", "状态", "User-Agent", "耗时(ms)", "错误信息"})
|
||
|
||
for _, l := range logs {
|
||
statusStr := "成功"
|
||
if l.Status != 0 {
|
||
statusStr = "失败"
|
||
}
|
||
_ = writer.Write([]string{
|
||
fmt.Sprintf("%d", l.ID),
|
||
l.CreatedAt.Format("2006-01-02 15:04:05"),
|
||
l.Username,
|
||
l.IP,
|
||
l.Module,
|
||
l.Action,
|
||
l.Resource,
|
||
l.ResourceID,
|
||
statusStr,
|
||
l.UserAgent,
|
||
fmt.Sprintf("%d", l.Duration),
|
||
l.Error,
|
||
})
|
||
}
|
||
writer.Flush()
|
||
|
||
// 记录审计日志
|
||
if uid, exists := c.Get("userID"); exists {
|
||
if u, ok := uid.(uint); ok {
|
||
username, _ := c.Get("username")
|
||
un, _ := username.(string)
|
||
h.RecordAuditLog(u, un, "audit", "export", fmt.Sprintf("导出审计日志(%d条)", len(logs)), nil)
|
||
}
|
||
}
|
||
|
||
filename := fmt.Sprintf("audit-logs-%s.csv", time.Now().Format("20060102-150405"))
|
||
c.Header("Content-Type", "text/csv; charset=utf-8")
|
||
c.Header("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"`, filename))
|
||
c.Data(http.StatusOK, "text/csv; charset=utf-8", buf.Bytes())
|
||
}
|
||
}
|
||
|
||
// GetAuditStatistics 获取审计统计
|
||
// @Summary 获取审计统计
|
||
// @Description 获取审计日志统计信息
|
||
// @Tags 审计日志
|
||
// @Accept json
|
||
// @Produce json
|
||
// @Param days query int false "统计天数" default(7)
|
||
// @Success 200 {object} map[string]interface{}
|
||
// @Router /api/v1/audit/statistics [get]
|
||
func (h *AuditHandler) GetAuditStatistics(c *gin.Context) {
|
||
days, _ := strconv.Atoi(c.DefaultQuery("days", "7"))
|
||
if days <= 0 {
|
||
days = 7
|
||
}
|
||
|
||
startTime := time.Now().AddDate(0, 0, -days)
|
||
|
||
// 统计各模块的操作数量
|
||
var moduleStats []struct {
|
||
Module string `json:"module"`
|
||
Count int64 `json:"count"`
|
||
}
|
||
h.db.Model(&model.AuditLog{}).
|
||
Select("module, COUNT(*) as count").
|
||
Where("created_at >= ?", startTime).
|
||
Group("module").
|
||
Scan(&moduleStats)
|
||
|
||
// 统计各操作类型的数量
|
||
var actionStats []struct {
|
||
Action string `json:"action"`
|
||
Count int64 `json:"count"`
|
||
}
|
||
h.db.Model(&model.AuditLog{}).
|
||
Select("action, COUNT(*) as count").
|
||
Where("created_at >= ?", startTime).
|
||
Group("action").
|
||
Scan(&actionStats)
|
||
|
||
// 统计每日操作数量
|
||
var dailyStats []struct {
|
||
Date string `json:"date"`
|
||
Count int64 `json:"count"`
|
||
}
|
||
h.db.Model(&model.AuditLog{}).
|
||
Select("DATE(created_at) as date, COUNT(*) as count").
|
||
Where("created_at >= ?", startTime).
|
||
Group("DATE(created_at)").
|
||
Order("date").
|
||
Scan(&dailyStats)
|
||
|
||
// 总记录数
|
||
var totalCount int64
|
||
h.db.Model(&model.AuditLog{}).Count(&totalCount)
|
||
|
||
// 今日记录数
|
||
today := time.Now().Format("2006-01-02")
|
||
var todayCount int64
|
||
h.db.Model(&model.AuditLog{}).
|
||
Where("DATE(created_at) = ?", today).
|
||
Count(&todayCount)
|
||
|
||
c.JSON(http.StatusOK, model.Response{Code: 200, Data: gin.H{
|
||
"total_count": totalCount,
|
||
"today_count": todayCount,
|
||
"module_stats": moduleStats,
|
||
"action_stats": actionStats,
|
||
"daily_stats": dailyStats,
|
||
}}) // Logan.Li
|
||
}
|
||
|
||
// ClearAuditLogs 清理审计日志
|
||
// @Summary 清理审计日志
|
||
// @Description 清理指定日期之前的审计日志
|
||
// @Tags 审计日志
|
||
// @Accept json
|
||
// @Produce json
|
||
// @Param request body map[string]string true "清理请求"
|
||
// @Success 200 {object} map[string]interface{}
|
||
// @Router /api/v1/audit/logs/clear [post]
|
||
func (h *AuditHandler) ClearAuditLogs(c *gin.Context) {
|
||
var req struct {
|
||
BeforeDate string `json:"before_date" binding:"required"`
|
||
}
|
||
|
||
if err := c.ShouldBindJSON(&req); err != nil {
|
||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "无效的请求参数"})
|
||
return
|
||
}
|
||
|
||
// 解析日期
|
||
beforeDate, err := time.Parse("2006-01-02", req.BeforeDate)
|
||
if err != nil {
|
||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "无效的日期格式"})
|
||
return
|
||
}
|
||
|
||
// 删除指定日期之前的日志
|
||
result := h.db.Where("created_at < ?", beforeDate).Delete(&model.AuditLog{})
|
||
if result.Error != nil {
|
||
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "清理失败"})
|
||
return
|
||
}
|
||
|
||
// 记录审计日志
|
||
userID, _ := c.Get("userID")
|
||
username, _ := c.Get("username")
|
||
h.RecordAuditLog(userID.(uint), username.(string), "audit", "clear", "清理审计日志", gin.H{
|
||
"before_date": req.BeforeDate,
|
||
"deleted": result.RowsAffected,
|
||
})
|
||
|
||
c.JSON(http.StatusOK, model.Response{Code: 200, Message: "清理成功", Data: gin.H{
|
||
"deleted": result.RowsAffected,
|
||
}}) // Logan.Li
|
||
}
|
||
|
||
// RecordAuditLog 记录审计日志
|
||
func (h *AuditHandler) RecordAuditLog(userID uint, username, module, action, description string, details interface{}) {
|
||
uid := userID
|
||
log := model.AuditLog{
|
||
UserID: &uid,
|
||
Username: username,
|
||
Module: module,
|
||
Action: action,
|
||
Resource: description,
|
||
}
|
||
|
||
h.db.Create(&log)
|
||
}
|