bastion 0.54.0
This commit is contained in:
@@ -0,0 +1,154 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// The test binary never writes to the real system log: the suite's audit
|
||||
// rows would otherwise land in the journal of whatever machine runs it.
|
||||
func init() { auditSink = func([]byte) {} }
|
||||
|
||||
// auditSinkCapture swaps the system-log sink for one that keeps the lines.
|
||||
func auditSinkCapture(t *testing.T) func() []map[string]any {
|
||||
t.Helper()
|
||||
var mu sync.Mutex
|
||||
var lines [][]byte
|
||||
prev := auditSink
|
||||
auditSink = func(line []byte) {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
lines = append(lines, append([]byte(nil), line...))
|
||||
}
|
||||
t.Cleanup(func() { auditSink = prev })
|
||||
return func() []map[string]any {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
var out []map[string]any
|
||||
for _, l := range lines {
|
||||
m := map[string]any{}
|
||||
if err := json.Unmarshal(l, &m); err != nil {
|
||||
t.Fatalf("system-log line is not JSON: %s", l)
|
||||
}
|
||||
out = append(out, m)
|
||||
}
|
||||
return out
|
||||
}
|
||||
}
|
||||
|
||||
func auditFileRows(t *testing.T, path string) []map[string]any {
|
||||
t.Helper()
|
||||
fh, err := os.Open(path)
|
||||
if err != nil {
|
||||
t.Fatalf("audit file %s: %v", path, err)
|
||||
}
|
||||
defer fh.Close()
|
||||
var out []map[string]any
|
||||
sc := bufio.NewScanner(fh)
|
||||
for sc.Scan() {
|
||||
m := map[string]any{}
|
||||
if err := json.Unmarshal(sc.Bytes(), &m); err != nil {
|
||||
t.Fatalf("audit row is not JSON: %s", sc.Text())
|
||||
}
|
||||
out = append(out, m)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// auditSameRow: the system-log line is the file's row, every field equal,
|
||||
// with `audit` and `pairing` added.
|
||||
func auditSameRow(t *testing.T, what string, file, logged map[string]any, wantFile, wantPairing string) {
|
||||
t.Helper()
|
||||
if logged["audit"] != wantFile || logged["pairing"] != wantPairing {
|
||||
t.Fatalf("%s: system-log line names audit=%v pairing=%v, want %q %q", what, logged["audit"], logged["pairing"], wantFile, wantPairing)
|
||||
}
|
||||
for k, v := range file {
|
||||
if lv, ok := logged[k]; !ok || !jsonEq(lv, v) {
|
||||
t.Fatalf("%s: field %q is %v in the file and %v in the system log", what, k, v, logged[k])
|
||||
}
|
||||
}
|
||||
if len(logged) != len(file)+2 {
|
||||
t.Fatalf("%s: system-log line has %d fields, the file row %d (+2 expected)", what, len(logged), len(file))
|
||||
}
|
||||
}
|
||||
|
||||
func jsonEq(a, b any) bool {
|
||||
ja, _ := json.Marshal(a)
|
||||
jb, _ := json.Marshal(b)
|
||||
return string(ja) == string(jb)
|
||||
}
|
||||
|
||||
// TestAuditRowsReachTheSystemLog: every audit writer on this box sends the row
|
||||
// it writes to its file to the system log as well, unchanged, naming the file
|
||||
// and the pairing: the management lane (mgmt_audit.jsonl), pairing changes
|
||||
// (pairing_audit.jsonl), and a pairing's audit.jsonl for store ops and op runs,
|
||||
// terminals and root commands.
|
||||
func TestAuditRowsReachTheSystemLog(t *testing.T) {
|
||||
t.Setenv("XDG_CONFIG_HOME", t.TempDir())
|
||||
t.Setenv("HOME", t.TempDir())
|
||||
got := auditSinkCapture(t)
|
||||
cfgDir, err := configDir()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
pairDir := filepath.Join(t.TempDir(), "acme.host.example")
|
||||
if err := os.MkdirAll(pairDir, 0o700); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
mgmtAudit("acme.host.example", "uptime", "observe", "allowed", "", 0, []string{"a"}, "")
|
||||
pairingAudit("pairing_add", "other.host.example", "ok")
|
||||
dcnAudit(pairDir, "dcn_read", 7, "/srv/notes.txt", "allowed")
|
||||
sessionAudit(pairDir, "session", "s-1", "ops", "start", 0, "opened")
|
||||
rootExecAudit(pairDir, map[string]any{"session_id": "s-2", "event": "stop", "exit": 0})
|
||||
|
||||
lines := got()
|
||||
if len(lines) != 5 {
|
||||
t.Fatalf("got %d system-log lines, want 5: %v", len(lines), lines)
|
||||
}
|
||||
mgmt := auditFileRows(t, filepath.Join(cfgDir, mgmtAuditFile))
|
||||
pair := auditFileRows(t, filepath.Join(cfgDir, auditFilePairing))
|
||||
per := auditFileRows(t, filepath.Join(pairDir, auditFilePairingD))
|
||||
if len(mgmt) != 1 || len(pair) != 1 || len(per) != 3 {
|
||||
t.Fatalf("file rows: mgmt %d, pairing %d, per-pairing %d; want 1, 1, 3", len(mgmt), len(pair), len(per))
|
||||
}
|
||||
auditSameRow(t, "management lane", mgmt[0], lines[0], mgmtAuditFile, "acme.host.example")
|
||||
auditSameRow(t, "pairing change", pair[0], lines[1], auditFilePairing, "")
|
||||
auditSameRow(t, "store op", per[0], lines[2], auditFilePairingD, "acme.host.example")
|
||||
auditSameRow(t, "terminal", per[1], lines[3], auditFilePairingD, "acme.host.example")
|
||||
auditSameRow(t, "root command", per[2], lines[4], auditFilePairingD, "acme.host.example")
|
||||
}
|
||||
|
||||
// TestAuditFileRowStandsWithoutASystemLog: a system log that takes nothing
|
||||
// costs the copy only; the row is still written to its file.
|
||||
func TestAuditFileRowStandsWithoutASystemLog(t *testing.T) {
|
||||
prev := auditSink
|
||||
auditSink = func([]byte) {}
|
||||
t.Cleanup(func() { auditSink = prev })
|
||||
pairDir := filepath.Join(t.TempDir(), "acme.host.example")
|
||||
if err := os.MkdirAll(pairDir, 0o700); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
dcnAudit(pairDir, "dcn_read", 1, "/x", "allowed")
|
||||
if rows := auditFileRows(t, filepath.Join(pairDir, auditFilePairingD)); len(rows) != 1 {
|
||||
t.Fatalf("file rows = %d, want 1", len(rows))
|
||||
}
|
||||
}
|
||||
|
||||
// TestAuditSystemLogKeepsNumbersExact: a large id goes to the system log as
|
||||
// the digits the file has, not as a float.
|
||||
func TestAuditSystemLogKeepsNumbersExact(t *testing.T) {
|
||||
var line string
|
||||
prev := auditSink
|
||||
auditSink = func(l []byte) { line = string(l) }
|
||||
t.Cleanup(func() { auditSink = prev })
|
||||
auditToSystemLog(auditFilePairingD, "p", []byte(`{"store_id":9007199254740993}`))
|
||||
if !json.Valid([]byte(line)) || !strings.Contains(line, `"store_id":9007199254740993`) {
|
||||
t.Fatalf("system-log line %s does not carry the id exactly", line)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user