mirror of
https://github.com/Awuqing/BackupX.git
synced 2026-05-11 09:59:56 +08:00
* 功能: v2.1 可观测性与流控 — Prometheus + 节点带宽 + 审计 Webhook 核心能力: - Prometheus /metrics 端点:11 类指标(任务/存储/节点/SLA/验证/恢复/复制) - 节点级带宽限速生效:model.Node.BandwidthLimit 覆盖全局默认 - 审计日志 Webhook 外输:HMAC-SHA256 签名,配合 SIEM 合规留档 实现: - server/internal/metrics/ 独立 Registry + 异步 Gauge Collector(30s) - backup/restore/verify/replication 服务注入 metrics 钩子,nil 安全 - resolveProviderForNode() 按 task.NodeID 解析 BandwidthLimit - AuditService.SetWebhook + 动态 settings 推送,无需重启 测试: - metrics/registry_test.go: 注册/采集/nil safety/HTTP handler - service/audit_service_webhook_test.go: 签名正确性/异步投递/禁用路径 - go test ./... 全部通过 * chore: 触发 CodeQL 扫描
181 lines
5.1 KiB
Go
181 lines
5.1 KiB
Go
package service
|
||
|
||
import (
|
||
"bytes"
|
||
"context"
|
||
"crypto/hmac"
|
||
"crypto/sha256"
|
||
"encoding/hex"
|
||
"encoding/json"
|
||
"fmt"
|
||
"log"
|
||
"net/http"
|
||
"strings"
|
||
"sync"
|
||
"time"
|
||
|
||
"backupx/server/internal/apperror"
|
||
"backupx/server/internal/model"
|
||
"backupx/server/internal/repository"
|
||
)
|
||
|
||
// AuditEntry 是记录审计日志的输入结构
|
||
type AuditEntry struct {
|
||
UserID uint
|
||
Username string
|
||
Category string // auth / storage_target / backup_task / backup_record / settings
|
||
Action string // create / update / delete / login_success / login_failed / ...
|
||
TargetType string
|
||
TargetID string
|
||
TargetName string
|
||
Detail string
|
||
ClientIP string
|
||
}
|
||
|
||
type AuditService struct {
|
||
repo repository.AuditLogRepository
|
||
|
||
// webhook 外输配置(可选)
|
||
webhookMu sync.RWMutex
|
||
webhookURL string
|
||
webhookSecret string
|
||
httpClient *http.Client
|
||
}
|
||
|
||
func NewAuditService(repo repository.AuditLogRepository) *AuditService {
|
||
return &AuditService{
|
||
repo: repo,
|
||
httpClient: &http.Client{
|
||
Timeout: 3 * time.Second, // 短超时:审计 webhook 不应拖慢业务
|
||
},
|
||
}
|
||
}
|
||
|
||
// SetWebhook 动态配置审计事件转发 URL 与签名密钥。
|
||
// - url 为空字符串时禁用转发
|
||
// - secret 非空时对 payload 计算 HMAC-SHA256,作为 X-BackupX-Signature header
|
||
//
|
||
// 适用场景:
|
||
// - 企业 SIEM 集成(Splunk HEC、ELK、Loki)
|
||
// - 安全审计留痕到第三方 WORM 存储
|
||
// - 合规日志归档(GDPR / SOC2)
|
||
func (s *AuditService) SetWebhook(url, secret string) {
|
||
if s == nil {
|
||
return
|
||
}
|
||
s.webhookMu.Lock()
|
||
defer s.webhookMu.Unlock()
|
||
s.webhookURL = strings.TrimSpace(url)
|
||
s.webhookSecret = strings.TrimSpace(secret)
|
||
}
|
||
|
||
// Record 异步 fire-and-forget 写入审计日志,不阻塞业务逻辑
|
||
func (s *AuditService) Record(entry AuditEntry) {
|
||
if s == nil || s.repo == nil {
|
||
return
|
||
}
|
||
go func() {
|
||
record := &model.AuditLog{
|
||
UserID: entry.UserID,
|
||
Username: entry.Username,
|
||
Category: entry.Category,
|
||
Action: entry.Action,
|
||
TargetType: entry.TargetType,
|
||
TargetID: entry.TargetID,
|
||
TargetName: entry.TargetName,
|
||
Detail: entry.Detail,
|
||
ClientIP: entry.ClientIP,
|
||
}
|
||
if err := s.repo.Create(context.Background(), record); err != nil {
|
||
log.Printf("[audit] failed to write audit log: %v", err)
|
||
}
|
||
s.fireWebhook(record)
|
||
}()
|
||
}
|
||
|
||
// fireWebhook 异步向外部系统转发审计事件。失败降级到本地日志,永不影响主流程。
|
||
func (s *AuditService) fireWebhook(record *model.AuditLog) {
|
||
if s == nil {
|
||
return
|
||
}
|
||
s.webhookMu.RLock()
|
||
url := s.webhookURL
|
||
secret := s.webhookSecret
|
||
s.webhookMu.RUnlock()
|
||
if url == "" {
|
||
return
|
||
}
|
||
payload := map[string]any{
|
||
"eventType": "audit.log",
|
||
"occurredAt": record.CreatedAt.UTC().Format(time.RFC3339),
|
||
"actor": map[string]any{
|
||
"userId": record.UserID,
|
||
"username": record.Username,
|
||
},
|
||
"category": record.Category,
|
||
"action": record.Action,
|
||
"targetType": record.TargetType,
|
||
"targetId": record.TargetID,
|
||
"targetName": record.TargetName,
|
||
"detail": record.Detail,
|
||
"clientIp": record.ClientIP,
|
||
}
|
||
body, err := json.Marshal(payload)
|
||
if err != nil {
|
||
log.Printf("[audit] webhook marshal failed: %v", err)
|
||
return
|
||
}
|
||
req, err := http.NewRequestWithContext(context.Background(), http.MethodPost, url, bytes.NewReader(body))
|
||
if err != nil {
|
||
log.Printf("[audit] webhook build request failed: %v", err)
|
||
return
|
||
}
|
||
req.Header.Set("Content-Type", "application/json")
|
||
req.Header.Set("User-Agent", "BackupX-Audit/1.0")
|
||
if secret != "" {
|
||
mac := hmac.New(sha256.New, []byte(secret))
|
||
mac.Write(body)
|
||
req.Header.Set("X-BackupX-Signature", "sha256="+hex.EncodeToString(mac.Sum(nil)))
|
||
}
|
||
resp, err := s.httpClient.Do(req)
|
||
if err != nil {
|
||
log.Printf("[audit] webhook POST failed: %v", err)
|
||
return
|
||
}
|
||
defer resp.Body.Close()
|
||
if resp.StatusCode >= 400 {
|
||
log.Printf("[audit] webhook returned status %d", resp.StatusCode)
|
||
}
|
||
}
|
||
|
||
// List 分页查询审计日志
|
||
func (s *AuditService) List(ctx context.Context, category string, limit, offset int) (*repository.AuditLogListResult, error) {
|
||
result, err := s.repo.List(ctx, repository.AuditLogListOptions{
|
||
Category: category,
|
||
Limit: limit,
|
||
Offset: offset,
|
||
})
|
||
if err != nil {
|
||
return nil, apperror.Internal("AUDIT_LOG_LIST_FAILED", fmt.Sprintf("无法获取审计日志列表: %v", err), err)
|
||
}
|
||
return result, nil
|
||
}
|
||
|
||
// ListAdvanced 多字段筛选分页查询(合规审计常用)。
|
||
func (s *AuditService) ListAdvanced(ctx context.Context, opts repository.AuditLogListOptions) (*repository.AuditLogListResult, error) {
|
||
result, err := s.repo.List(ctx, opts)
|
||
if err != nil {
|
||
return nil, apperror.Internal("AUDIT_LOG_LIST_FAILED", fmt.Sprintf("无法获取审计日志: %v", err), err)
|
||
}
|
||
return result, nil
|
||
}
|
||
|
||
// ExportAll 返回指定筛选条件下的全部审计日志(最多 10000 条),用于 CSV 导出。
|
||
func (s *AuditService) ExportAll(ctx context.Context, opts repository.AuditLogListOptions) ([]model.AuditLog, error) {
|
||
items, err := s.repo.ListAll(ctx, opts)
|
||
if err != nil {
|
||
return nil, apperror.Internal("AUDIT_LOG_EXPORT_FAILED", fmt.Sprintf("无法导出审计日志: %v", err), err)
|
||
}
|
||
return items, nil
|
||
}
|