feat(audit): DegradingSink + durable buffer + loki/ntfy + reconcile (#54)
The classification-aware I5 audit path (§4.4): - DegradingSink.Reserve: central up → AuditCentral; central down + confidential → refuse (no buffer); central down + internal/public + buffer writable → AuditBuffered; central down + buffer unwritable → floor refuse. Record executes the reserved outcome and, when buffered, fires an ntfy alert. - FileBuffer: durable JSONL buffer that survives process restart; Confirm rewrites the file without a record, so a buffered record is cleared ONLY after its central write is confirmed. - LokiCentral: /ready probe + /loki/api/v1/push (full audit entry as the structured line). NtfyNotifier: degraded-state alerts; token only in the auth header, never logged (regression-tested). - Reconcile + StartReconcile: replay buffered records to central on recovery, confirm-then-clear per record; a failed push keeps the record buffered (no loss). SlogSink updated to the two-phase shape (always central, never fails) — the default when no loki endpoint is set. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -11,11 +11,14 @@ import (
|
||||
"log/slog"
|
||||
|
||||
"github.com/mathiasbq/hyperguild/ingestion/internal/capture"
|
||||
"github.com/mathiasbq/hyperguild/ingestion/internal/classification"
|
||||
)
|
||||
|
||||
// SlogSink records audit entries to an slog.Logger. It never fails, so it
|
||||
// does not exercise the I5 floor (refuse-if-unauditable) — that is #54's
|
||||
// loki+buffer sink. A nil logger falls back to slog.Default().
|
||||
// SlogSink records audit entries to an slog.Logger. It never fails and is
|
||||
// always centrally available, so its Reserve always grants AuditCentral —
|
||||
// it does not exercise the I5 degradation/floor. That is DegradingSink's
|
||||
// job (loki + durable buffer). SlogSink is the default for deployments
|
||||
// without a loki endpoint configured. A nil logger ⇒ slog.Default().
|
||||
type SlogSink struct {
|
||||
logger *slog.Logger
|
||||
}
|
||||
@@ -28,10 +31,15 @@ func NewSlogSink(logger *slog.Logger) *SlogSink {
|
||||
return &SlogSink{logger: logger}
|
||||
}
|
||||
|
||||
// Reserve always grants central recording — slog is always available.
|
||||
func (s *SlogSink) Reserve(_ context.Context, _ classification.Level) (capture.AuditOutcome, error) {
|
||||
return capture.AuditCentral, nil
|
||||
}
|
||||
|
||||
// Record emits the audit entry at info level. Security events, when
|
||||
// present, are logged at warn level so they surface independently of the
|
||||
// routine audit stream.
|
||||
func (s *SlogSink) Record(_ context.Context, e capture.AuditEntry) error {
|
||||
func (s *SlogSink) Record(_ context.Context, e capture.AuditEntry, _ capture.AuditOutcome) error {
|
||||
s.logger.Info("capture audit",
|
||||
"principal", e.Principal,
|
||||
"actor", e.Actor,
|
||||
|
||||
Reference in New Issue
Block a user