Files
privatae-bastion/client-go/auditlog_queue_test.go
T
Privatae d62da8adb4
ci / personal-data (push) Successful in 4s
ci / fuzz (push) Successful in 2m44s
ci / test (push) Successful in 2m11s
bastion 0.54.0
2026-10-21 22:29:10 +00:00

96 lines
3.1 KiB
Go

package main
import (
"encoding/json"
"reflect"
"strings"
"sync"
"testing"
"time"
)
// The sink the binary ships with, read before auditlog_test.go's init swaps
// it out (package variables are set before any init runs).
var auditSinkShipped = auditSink
// TestAuditSinkIsTheQueue: the shipped sink is the queue, never the
// platform's writer called on the audited op's own goroutine.
func TestAuditSinkIsTheQueue(t *testing.T) {
if reflect.ValueOf(auditSinkShipped).Pointer() != reflect.ValueOf(auditEnqueue).Pointer() {
t.Fatal("the system-log sink must be auditEnqueue: a direct write blocks the audited op while syslog is stuck")
}
}
// TestAuditQueueNeverWaitsOnAStuckWriter: with a writer that never returns,
// far more rows than the queue holds are all taken at once; the ones past the
// bound are dropped and counted, and the count rides on the next line that
// gets into the queue.
func TestAuditQueueNeverWaitsOnAStuckWriter(t *testing.T) {
release := make(chan struct{})
var mu sync.Mutex
var got []string
q := newAuditQueue(8, func(l []byte) {
<-release
mu.Lock()
got = append(got, string(l))
mu.Unlock()
})
done := make(chan struct{})
go func() {
for i := 0; i < 1000; i++ {
q.enqueue([]byte(`{"n":1}`))
}
close(done)
}()
select {
case <-done:
case <-time.After(5 * time.Second):
t.Fatal("audit rows waited on a stuck system log")
}
close(release)
q.flush(5 * time.Second)
// Nothing lost without a count: every one of the 1001 rows is either a
// line written or counted on a line written after the gap.
q.enqueue([]byte(`{"n":2}`))
q.flush(5 * time.Second)
mu.Lock()
defer mu.Unlock()
var counted float64
for _, l := range got {
m := map[string]any{}
if err := json.Unmarshal([]byte(l), &m); err != nil {
t.Fatal(err)
}
if n, ok := m[auditDroppedField].(float64); ok {
counted += n
}
}
if len(got) > 10 || counted == 0 || float64(len(got))+counted != 1001 {
t.Fatalf("lines written %d (at most 10), dropped and counted %v: want 1001 in all", len(got), counted)
}
}
// TestAuditSystemLogOmitsTheRootCommandText: a root command's text stays in
// the 0600 file; the system-log line carries its digest and length.
func TestAuditSystemLogOmitsTheRootCommandText(t *testing.T) {
var line string
prev := auditSink
auditSink = func(l []byte) { line = string(l) }
t.Cleanup(func() { auditSink = prev })
auditToSystemLog(auditFilePairingD, "p", []byte(`{"event":"start","command":"PGPASSWORD=hunter2 psql","command_sha256":"ab"}`))
m := map[string]any{}
if err := json.Unmarshal([]byte(line), &m); err != nil {
t.Fatal(err)
}
if strings.Contains(line, "hunter2") || m["command"] != nil {
t.Fatalf("the command text reached the system log: %s", line)
}
if m["command_sha256"] != "ab" || m["command_bytes"] != float64(len("PGPASSWORD=hunter2 psql")) {
t.Fatalf("the line must keep the digest and the length: %s", line)
}
auditToSystemLog(auditFilePairingD, "p", []byte(`{"command":"x","command_bytes":70000}`))
if !strings.Contains(line, `"command_bytes":70000`) {
t.Fatalf("a row's own command_bytes (the full length) is kept: %s", line)
}
}