mirror of
https://github.com/Ed1s0nZ/CyberStrikeAI.git
synced 2026-05-22 07:36:58 +02:00
167 lines
3.9 KiB
Go
167 lines
3.9 KiB
Go
package audit
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"strings"
|
|
"time"
|
|
|
|
"cyberstrike-ai/internal/config"
|
|
"cyberstrike-ai/internal/database"
|
|
"cyberstrike-ai/internal/security"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/google/uuid"
|
|
"go.uber.org/zap"
|
|
)
|
|
|
|
// Service persists platform audit logs.
|
|
type Service struct {
|
|
db *database.DB
|
|
cfg *config.Config
|
|
logger *zap.Logger
|
|
failThrottle *failureThrottle
|
|
}
|
|
|
|
// NewService creates an audit service.
|
|
func NewService(db *database.DB, cfg *config.Config, logger *zap.Logger) *Service {
|
|
return &Service{
|
|
db: db,
|
|
cfg: cfg,
|
|
logger: logger,
|
|
failThrottle: newFailureThrottle(),
|
|
}
|
|
}
|
|
|
|
// Enabled reports whether audit persistence is on.
|
|
func (s *Service) Enabled() bool {
|
|
if s == nil || s.cfg == nil {
|
|
return false
|
|
}
|
|
return s.cfg.Audit.EnabledEffective()
|
|
}
|
|
|
|
// Record writes one audit row from a Gin request context.
|
|
func (s *Service) Record(c *gin.Context, e Entry) {
|
|
if s == nil || !s.Enabled() || s.db == nil {
|
|
return
|
|
}
|
|
if strings.TrimSpace(e.Category) == "" || strings.TrimSpace(e.Action) == "" {
|
|
return
|
|
}
|
|
if e.Result == "failure" && !s.allowFailureAudit(c, e) {
|
|
return
|
|
}
|
|
if strings.TrimSpace(e.Result) == "" {
|
|
e.Result = "success"
|
|
}
|
|
if strings.TrimSpace(e.Level) == "" {
|
|
if e.Result == "failure" {
|
|
e.Level = "warn"
|
|
} else {
|
|
e.Level = "info"
|
|
}
|
|
}
|
|
if strings.TrimSpace(e.Actor) == "" {
|
|
e.Actor = "admin"
|
|
}
|
|
if e.SessionHint == "" && c != nil {
|
|
if token := c.GetString(security.ContextAuthTokenKey); token != "" {
|
|
e.SessionHint = sessionHint(token)
|
|
}
|
|
}
|
|
maxDetail := s.cfg.Audit.MaxDetailBytesEffective()
|
|
detail := SanitizeDetail(e.Detail, maxDetail)
|
|
|
|
row := &database.AuditLog{
|
|
ID: "audit_" + strings.ReplaceAll(uuid.New().String(), "-", ""),
|
|
CreatedAt: time.Now(),
|
|
Level: e.Level,
|
|
Category: e.Category,
|
|
Action: e.Action,
|
|
Result: e.Result,
|
|
Actor: e.Actor,
|
|
SessionHint: e.SessionHint,
|
|
ClientIP: clientIP(c),
|
|
UserAgent: userAgent(c),
|
|
ResourceType: e.ResourceType,
|
|
ResourceID: e.ResourceID,
|
|
Message: e.Message,
|
|
Detail: detail,
|
|
}
|
|
if err := s.db.AppendAuditLog(row); err != nil && s.logger != nil {
|
|
s.logger.Warn("写入审计日志失败",
|
|
zap.String("action", e.Action),
|
|
zap.Error(err),
|
|
)
|
|
}
|
|
}
|
|
|
|
// RecordSystem writes an audit row without HTTP context (e.g. retention cleanup).
|
|
func (s *Service) RecordSystem(e Entry) {
|
|
s.Record(nil, e)
|
|
}
|
|
|
|
// PurgeExpired deletes rows older than retention_days when configured.
|
|
func (s *Service) PurgeExpired() {
|
|
if s == nil || s.db == nil || s.cfg == nil {
|
|
return
|
|
}
|
|
days := s.cfg.Audit.RetentionDaysEffective()
|
|
if days <= 0 {
|
|
return
|
|
}
|
|
cutoff := time.Now().AddDate(0, 0, -days)
|
|
n, err := s.db.DeleteAuditLogsBefore(cutoff)
|
|
if err != nil {
|
|
if s.logger != nil {
|
|
s.logger.Warn("清理过期审计日志失败", zap.Error(err))
|
|
}
|
|
return
|
|
}
|
|
if n > 0 && s.logger != nil {
|
|
s.logger.Info("已清理过期审计日志", zap.Int64("deleted", n))
|
|
}
|
|
}
|
|
|
|
// HintFromToken returns a short stable hash prefix for a session token.
|
|
func HintFromToken(token string) string {
|
|
return sessionHint(token)
|
|
}
|
|
|
|
func sessionHint(token string) string {
|
|
token = strings.TrimSpace(token)
|
|
if token == "" {
|
|
return ""
|
|
}
|
|
sum := sha256.Sum256([]byte(token))
|
|
return hex.EncodeToString(sum[:4])
|
|
}
|
|
|
|
func (s *Service) allowFailureAudit(c *gin.Context, e Entry) bool {
|
|
if !isAuthFailureThrottled(e.Category, e.Action) {
|
|
return true
|
|
}
|
|
cooldown := time.Duration(s.cfg.Audit.AuthFailureCooldownEffective()) * time.Second
|
|
key := authFailureThrottleKey(e.Category, e.Action, clientIP(c))
|
|
return s.failThrottle.allow(key, cooldown)
|
|
}
|
|
|
|
func clientIP(c *gin.Context) string {
|
|
if c == nil {
|
|
return ""
|
|
}
|
|
return c.ClientIP()
|
|
}
|
|
|
|
func userAgent(c *gin.Context) string {
|
|
if c == nil {
|
|
return ""
|
|
}
|
|
ua := c.GetHeader("User-Agent")
|
|
if len(ua) > 512 {
|
|
return ua[:512]
|
|
}
|
|
return ua
|
|
}
|