mirror of
https://github.com/MengMengCode/VoCat.git
synced 2026-08-21 15:23:43 +08:00
- Added MaxLogEvents constant to enforce a hard limit on stored log events. - Updated AppendLogEvent to discard older logs when the limit is exceeded. - Enhanced ListLogEvents to support filtering by log level and excluding specific messages. - Introduced ClearLogEvents method to permanently remove logs and prevent re-queuing of cleared entries. - Modified LogRetentionCard component to reflect the new log retention settings and limits. - Added logging categories for better organization and filtering in the UI. - Implemented sanitization for sensitive information in logs. - Added tests for log event limits and clearing functionality.
101 lines
2.9 KiB
Go
101 lines
2.9 KiB
Go
package server
|
|
|
|
import (
|
|
"context"
|
|
"log/slog"
|
|
"net"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
|
|
"vocat/internal/store"
|
|
)
|
|
|
|
// recordAudit writes one security-relevant event to the audit trail. Failures
|
|
// are logged but never block the request being audited.
|
|
func (s *Server) recordAudit(
|
|
ctx context.Context,
|
|
actor string,
|
|
action string,
|
|
entityType string,
|
|
entityID string,
|
|
outcome string,
|
|
remoteAddr string,
|
|
) {
|
|
level := slog.LevelInfo
|
|
if !strings.EqualFold(strings.TrimSpace(outcome), "success") {
|
|
level = slog.LevelWarn
|
|
}
|
|
if s.logger != nil {
|
|
s.logger.Log(ctx, level, "user operation",
|
|
"category", auditLogCategory(action),
|
|
"event", action,
|
|
"actor", actor,
|
|
"entity_type", entityType,
|
|
"entity_id", entityID,
|
|
"outcome", outcome,
|
|
)
|
|
}
|
|
if s.store == nil {
|
|
return
|
|
}
|
|
_, err := s.store.AppendAuditEvent(ctx, store.AuditEvent{
|
|
Actor: actor,
|
|
Action: action,
|
|
EntityType: entityType,
|
|
EntityID: entityID,
|
|
Outcome: outcome,
|
|
RemoteAddr: remoteAddr,
|
|
CreatedAt: time.Now().UTC(),
|
|
})
|
|
if err != nil {
|
|
s.logger.Warn("write audit event failed", "category", "system", "action", action, "raw_error", err)
|
|
}
|
|
}
|
|
|
|
func auditLogCategory(action string) string {
|
|
action = strings.ToLower(strings.TrimSpace(action))
|
|
switch {
|
|
case strings.Contains(action, ".sms") || strings.HasPrefix(action, "sms."):
|
|
return "sms"
|
|
case strings.Contains(action, ".call") || strings.HasPrefix(action, "call."):
|
|
return "call"
|
|
case strings.Contains(action, "vowifi") || strings.Contains(action, "ims"):
|
|
return "vowifi"
|
|
case strings.Contains(action, "device") || strings.Contains(action, "esim") ||
|
|
strings.Contains(action, ".at.") || strings.Contains(action, ".ussd"):
|
|
return "hardware"
|
|
default:
|
|
return "operation"
|
|
}
|
|
}
|
|
|
|
// audit records an event for an already-authenticated request, resolving the
|
|
// actor from the session and the source address from the raw connection (proxy
|
|
// headers are deliberately not trusted for the audit trail).
|
|
func (s *Server) audit(r *http.Request, action string, entityType string, entityID string, outcome string) {
|
|
actor := ""
|
|
if s.auth != nil {
|
|
if cookie, err := r.Cookie(sessionCookieName); err == nil && cookie.Value != "" {
|
|
if session, authErr := s.auth.Authenticate(r.Context(), cookie.Value); authErr == nil {
|
|
actor = session.Principal.Username
|
|
}
|
|
}
|
|
}
|
|
s.recordAudit(r.Context(), actor, action, entityType, entityID, outcome, requestRemoteHost(r))
|
|
}
|
|
|
|
// auditAuth records an authentication event where no session exists yet (the
|
|
// actor is the username that was attempted).
|
|
func (s *Server) auditAuth(r *http.Request, username string, outcome string) {
|
|
s.recordAudit(r.Context(), username, "auth.login", "session", username, outcome, requestRemoteHost(r))
|
|
}
|
|
|
|
func requestRemoteHost(r *http.Request) string {
|
|
host, _, err := net.SplitHostPort(strings.TrimSpace(r.RemoteAddr))
|
|
if err != nil {
|
|
return strings.TrimSpace(r.RemoteAddr)
|
|
}
|
|
return host
|
|
}
|