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>
57 lines
2.0 KiB
Go
57 lines
2.0 KiB
Go
// Package audit provides AuditSink implementations for the capture
|
|
// capability (I5). This file ships the minimal slog-backed sink used in
|
|
// #53: it emits the request-level audit record to structured logs, which
|
|
// the alloy/loki substrate already scrapes. The classification-aware
|
|
// degradation/refusal sink (confidential fails closed, internal buffers +
|
|
// reconciles) lands in #54 and replaces this behind the same interface.
|
|
package audit
|
|
|
|
import (
|
|
"context"
|
|
"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 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
|
|
}
|
|
|
|
// NewSlogSink constructs a SlogSink. nil logger ⇒ slog.Default().
|
|
func NewSlogSink(logger *slog.Logger) *SlogSink {
|
|
if logger == nil {
|
|
logger = slog.Default()
|
|
}
|
|
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, _ capture.AuditOutcome) error {
|
|
s.logger.Info("capture audit",
|
|
"principal", e.Principal,
|
|
"actor", e.Actor,
|
|
"harness", e.Harness,
|
|
"session_ref", e.SessionRef,
|
|
"classification", e.EffectiveClassification,
|
|
"items", e.Items,
|
|
"ts", e.Timestamp,
|
|
)
|
|
for _, ev := range e.SecurityEvents {
|
|
s.logger.Warn("capture security event", "principal", e.Principal, "event", ev)
|
|
}
|
|
return nil
|
|
}
|