feat(audit): minimal slog AuditSink for capture I5 (#53)
Emits the request-level audit record to structured logs (scraped by the existing alloy/loki substrate) and surfaces security events at warn level. Placeholder behind the AuditSink interface — the classification- aware loki+buffer+reconcile sink (confidential fails closed, internal degrades) lands in #54 and replaces this without touching callers. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,48 @@
|
|||||||
|
// 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"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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().
|
||||||
|
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}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 {
|
||||||
|
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
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
package audit_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"log/slog"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/mathiasbq/hyperguild/ingestion/internal/audit"
|
||||||
|
"github.com/mathiasbq/hyperguild/ingestion/internal/capture"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSlogSinkRecordsEntryAndSecurityEvents(t *testing.T) {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
sink := audit.NewSlogSink(slog.New(slog.NewTextHandler(&buf, nil)))
|
||||||
|
|
||||||
|
err := sink.Record(context.Background(), capture.AuditEntry{
|
||||||
|
Principal: "koala-cli",
|
||||||
|
Harness: "claude-code",
|
||||||
|
EffectiveClassification: "confidential",
|
||||||
|
Items: []string{"insight:wiki/a/facts/x.md"},
|
||||||
|
SecurityEvents: []string{"asserted-vs-derived origin mismatch"},
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
out := buf.String()
|
||||||
|
assert.Contains(t, out, "capture audit")
|
||||||
|
assert.Contains(t, out, "koala-cli")
|
||||||
|
assert.Contains(t, out, "confidential")
|
||||||
|
assert.Contains(t, out, "capture security event")
|
||||||
|
assert.Contains(t, out, "asserted-vs-derived origin mismatch")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSlogSinkNilLoggerDefaults(t *testing.T) {
|
||||||
|
// nil logger must not panic.
|
||||||
|
require.NotPanics(t, func() {
|
||||||
|
_ = audit.NewSlogSink(nil).Record(context.Background(), capture.AuditEntry{})
|
||||||
|
})
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user