// Package audit provides structured audit event logging for Metacrypt. // Audit events record security-relevant operations (who did what, when, // and whether it succeeded) as one JSON object per line. package audit import ( "context" "log/slog" ) // LevelAudit is a custom slog level for audit events. It sits above Warn // so that audit events are never suppressed by log level filtering. const LevelAudit = slog.Level(12) func init() { // Replace the level name so JSON output shows "AUDIT" instead of "ERROR+4". slog.SetLogLoggerLevel(LevelAudit) } // Logger writes structured audit events. A nil *Logger is safe to use; // all methods are no-ops. type Logger struct { logger *slog.Logger } // New creates an audit logger writing to the given handler. Pass nil to // create a disabled logger (equivalent to using a nil *Logger). func New(h slog.Handler) *Logger { if h == nil { return nil } return &Logger{logger: slog.New(h)} } // Event represents a single audit event. type Event struct { Caller string // MCIAS username, or "operator" for unauthenticated ops Roles []string // caller's MCIAS roles Operation string // engine operation name (e.g., "issue", "sign-user") Engine string // engine type (e.g., "ca", "sshca", "transit", "user") Mount string // mount name Resource string // policy resource path evaluated Outcome string // "success", "denied", or "error" Error string // error message (on "error" or "denied" outcomes) Detail map[string]interface{} // operation-specific metadata } // Log writes an audit event. Safe to call on a nil receiver. func (l *Logger) Log(ctx context.Context, e Event) { if l == nil { return } attrs := []slog.Attr{ slog.String("caller", e.Caller), slog.String("operation", e.Operation), slog.String("outcome", e.Outcome), } if len(e.Roles) > 0 { attrs = append(attrs, slog.Any("roles", e.Roles)) } if e.Engine != "" { attrs = append(attrs, slog.String("engine", e.Engine)) } if e.Mount != "" { attrs = append(attrs, slog.String("mount", e.Mount)) } if e.Resource != "" { attrs = append(attrs, slog.String("resource", e.Resource)) } if e.Error != "" { attrs = append(attrs, slog.String("error", e.Error)) } if len(e.Detail) > 0 { attrs = append(attrs, slog.Any("detail", e.Detail)) } l.logger.LogAttrs(ctx, LevelAudit, "audit", attrs...) }