// Package audit provides a thin convenience wrapper around // engine.Audit tailored to mTLS / cert lifecycle events. It exists so // that cert, transport, and daemon code can call a small, semantically // clear API (Emit with explicit action + result) without depending on // the more general-purpose engine.Audit. package audit import ( "context" "fmt" "log/slog" "git.cloudinit.dev/coreci/orca/internal/engine" ) // Result enumerates the result strings persisted to audit_log. Keeping // these as constants (rather than free-form strings) prevents typos at // call sites and makes log analytics trivial. type Result string const ( ResultSuccess Result = "success" ResultFailure Result = "failure" ResultDenied Result = "denied" ) // Action enumerates the cert / handshake event names used across the // security surface. Matches REQ-038 / P01 must-haves: // // cert.issued — a CSR was signed, server cert persisted // cert.renewed — a server cert was re-issued (rotation) // cert.joined — a node joined the trust domain (CA pinned) // node.handshake_ok — mTLS handshake succeeded // node.handshake_failed — mTLS handshake failed type Action string const ( ActionCertIssued Action = "cert.issued" ActionCertRenewed Action = "cert.renewed" ActionCertJoined Action = "cert.joined" ActionNodeHandshakeOK Action = "node.handshake_ok" ActionNodeHandshakeFail Action = "node.handshake_failed" ) // Audit wraps engine.Audit with a cert/handshake-focused API. type Audit struct { engine *engine.Audit } // New constructs an Audit backed by the given engine.Audit. The engine // instance persists to the audit_log table; the wrapper just shapes // the call signature. func New(e *engine.Audit) *Audit { return &Audit{engine: e} } // Emit persists an audit entry. The `event` is a free-form description // that ends up in the resource field, paired with action + result. Use // the Action* constants for `action`; free-form strings for `event` are // allowed for extensibility but should be stable for analytics. func (a *Audit) Emit(ctx context.Context, action Action, event string, result Result, metadata map[string]any) { if a == nil || a.engine == nil { return } // Resource field is conventionally :; we just use event // as-is here. Callers can stuff the relevant id into metadata. a.engine.Record(ctx, "security", string(action), event, string(result), nil, metadata) } // EmitWithErr persists a failure entry whose err is also recorded in the // audit_log.error column. Use this for handshake failures and similar // error paths where the underlying error is useful for postmortem. func (a *Audit) EmitWithErr(ctx context.Context, action Action, event string, err error, metadata map[string]any) { if a == nil || a.engine == nil { return } a.engine.Record(ctx, "security", string(action), event, string(ResultFailure), err, metadata) } // LogHandshakeOK emits a structured slog record for a successful mTLS // handshake. This is a SEPARATE log line from the audit_log entry — // structured slog is for operators; audit_log is for compliance. func LogHandshakeOK(log *slog.Logger, peer, certFP string) { if log == nil { return } log.Info("mtls.handshake", slog.String("event", "mtls.handshake"), slog.String("result", "ok"), slog.String("peer", peer), slog.String("cert_fp", certFP), ) } // LogHandshakeFailed emits a structured slog record for a failed mTLS // handshake. Per REQ-038, the fields are: event=mtls.handshake, peer, // cert_fp (may be empty if no cert was presented), err. func LogHandshakeFailed(log *slog.Logger, peer, certFP string, err error) { if log == nil { return } attrs := []any{ slog.String("event", "mtls.handshake"), slog.String("result", "failed"), slog.String("peer", peer), slog.String("cert_fp", certFP), } if err != nil { attrs = append(attrs, slog.String("err", err.Error())) } log.Warn("mtls.handshake", attrs...) } // String converts an Action to its canonical string form. Useful in // tests and CLI surface. func (a Action) String() string { return string(a) } // String converts a Result to its canonical string form. func (r Result) String() string { return string(r) } // FormatAction formats an action+result pair as "action=... result=...", // used by callers building structured log lines. func FormatAction(action Action, result Result) string { return fmt.Sprintf("action=%s result=%s", action, result) }