monorepo/cloud/maplefile-backend/pkg/auditlog/auditlog.go

182 lines
6 KiB
Go

// Package auditlog provides security audit logging for compliance and security monitoring.
// Audit logs are separate from application logs and capture security-relevant events
// with consistent structure for analysis and alerting.
package auditlog
import (
"context"
"time"
"github.com/gocql/gocql"
"go.uber.org/zap"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config/constants"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/validation"
)
// EventType represents the type of security event
type EventType string
const (
// Authentication events
EventTypeLoginAttempt EventType = "login_attempt"
EventTypeLoginSuccess EventType = "login_success"
EventTypeLoginFailure EventType = "login_failure"
EventTypeLogout EventType = "logout"
EventTypeTokenRefresh EventType = "token_refresh"
EventTypeTokenRevoked EventType = "token_revoked"
// Account events
EventTypeAccountCreated EventType = "account_created"
EventTypeAccountDeleted EventType = "account_deleted"
EventTypeAccountLocked EventType = "account_locked"
EventTypeAccountUnlocked EventType = "account_unlocked"
EventTypeEmailVerified EventType = "email_verified"
// Recovery events
EventTypeRecoveryInitiated EventType = "recovery_initiated"
EventTypeRecoveryCompleted EventType = "recovery_completed"
EventTypeRecoveryFailed EventType = "recovery_failed"
// Access control events
EventTypeAccessDenied EventType = "access_denied"
EventTypePermissionChanged EventType = "permission_changed"
// Sharing events
EventTypeCollectionShared EventType = "collection_shared"
EventTypeCollectionUnshared EventType = "collection_unshared"
EventTypeSharingBlocked EventType = "sharing_blocked"
)
// Outcome represents the result of the audited action
type Outcome string
const (
OutcomeSuccess Outcome = "success"
OutcomeFailure Outcome = "failure"
OutcomeBlocked Outcome = "blocked"
)
// AuditEvent represents a security audit event
type AuditEvent struct {
Timestamp time.Time `json:"timestamp"`
EventType EventType `json:"event_type"`
Outcome Outcome `json:"outcome"`
UserID string `json:"user_id,omitempty"`
Email string `json:"email,omitempty"` // Always masked
ClientIP string `json:"client_ip,omitempty"`
UserAgent string `json:"user_agent,omitempty"`
Resource string `json:"resource,omitempty"`
Action string `json:"action,omitempty"`
Details map[string]string `json:"details,omitempty"`
FailReason string `json:"fail_reason,omitempty"`
}
// AuditLogger provides security audit logging functionality
type AuditLogger interface {
// Log records a security audit event
Log(ctx context.Context, event AuditEvent)
// LogAuth logs an authentication event with common fields
LogAuth(ctx context.Context, eventType EventType, outcome Outcome, email string, clientIP string, details map[string]string)
// LogAccess logs an access control event
LogAccess(ctx context.Context, eventType EventType, outcome Outcome, userID string, resource string, action string, details map[string]string)
}
type auditLoggerImpl struct {
logger *zap.Logger
}
// NewAuditLogger creates a new audit logger
func NewAuditLogger(logger *zap.Logger) AuditLogger {
// Create a named logger specifically for audit events
// This allows filtering audit logs separately from application logs
auditLogger := logger.Named("AUDIT")
return &auditLoggerImpl{
logger: auditLogger,
}
}
// Log records a security audit event
func (a *auditLoggerImpl) Log(ctx context.Context, event AuditEvent) {
// Set timestamp if not provided
if event.Timestamp.IsZero() {
event.Timestamp = time.Now().UTC()
}
// Build zap fields
fields := []zap.Field{
zap.String("audit_event", string(event.EventType)),
zap.String("outcome", string(event.Outcome)),
zap.Time("event_time", event.Timestamp),
}
if event.UserID != "" {
fields = append(fields, zap.String("user_id", event.UserID))
}
if event.Email != "" {
fields = append(fields, zap.String("email", validation.MaskEmail(event.Email))) // Always mask for safety
}
if event.ClientIP != "" {
fields = append(fields, zap.String("client_ip", validation.MaskIP(event.ClientIP))) // Always mask for safety
}
if event.UserAgent != "" {
fields = append(fields, zap.String("user_agent", event.UserAgent))
}
if event.Resource != "" {
fields = append(fields, zap.String("resource", event.Resource))
}
if event.Action != "" {
fields = append(fields, zap.String("action", event.Action))
}
if event.FailReason != "" {
fields = append(fields, zap.String("fail_reason", event.FailReason))
}
if len(event.Details) > 0 {
fields = append(fields, zap.Any("details", event.Details))
}
// Try to get request ID from context
if requestID, ok := ctx.Value(constants.SessionID).(string); ok && requestID != "" {
fields = append(fields, zap.String("request_id", requestID))
}
// Log at INFO level - audit events are always important
a.logger.Info("security_audit", fields...)
}
// LogAuth logs an authentication event with common fields
func (a *auditLoggerImpl) LogAuth(ctx context.Context, eventType EventType, outcome Outcome, email string, clientIP string, details map[string]string) {
event := AuditEvent{
Timestamp: time.Now().UTC(),
EventType: eventType,
Outcome: outcome,
Email: email, // Should be pre-masked by caller
ClientIP: clientIP,
Details: details,
}
// Extract user ID from context if available
if userID, ok := ctx.Value(constants.SessionUserID).(gocql.UUID); ok {
event.UserID = userID.String()
}
a.Log(ctx, event)
}
// LogAccess logs an access control event
func (a *auditLoggerImpl) LogAccess(ctx context.Context, eventType EventType, outcome Outcome, userID string, resource string, action string, details map[string]string) {
event := AuditEvent{
Timestamp: time.Now().UTC(),
EventType: eventType,
Outcome: outcome,
UserID: userID,
Resource: resource,
Action: action,
Details: details,
}
a.Log(ctx, event)
}