M1: hash-chained session audit log
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,77 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func auditFixture(t *testing.T) string {
|
||||
t.Helper()
|
||||
return filepath.Join(t.TempDir(), "audit.jsonl")
|
||||
}
|
||||
|
||||
func sampleSummary(id string) SessionSummary {
|
||||
return SessionSummary{
|
||||
SessionID: id,
|
||||
NextAction: "write plan",
|
||||
SuccessCond: "plan committed",
|
||||
Outcome: "completed",
|
||||
StartedUnix: 1000,
|
||||
EndedUnix: 2000,
|
||||
SwitchCount: 3,
|
||||
Buckets: []BucketTotal{
|
||||
{Class: "code", Title: "antidrift", Seconds: 540},
|
||||
{Class: "firefox", Title: "docs", Seconds: 120},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func TestAppendSessionChainsAndVerifies(t *testing.T) {
|
||||
path := auditFixture(t)
|
||||
for i := 0; i < 3; i++ {
|
||||
if err := AppendSession(path, sampleSummary("session-"+string(rune('a'+i)))); err != nil {
|
||||
t.Fatalf("append %d: %v", i, err)
|
||||
}
|
||||
}
|
||||
if err := VerifyChain(path); err != nil {
|
||||
t.Fatalf("chain should verify: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenesisPrevHashIsZeros(t *testing.T) {
|
||||
path := auditFixture(t)
|
||||
if err := AppendSession(path, sampleSummary("session-a")); err != nil {
|
||||
t.Fatalf("append: %v", err)
|
||||
}
|
||||
data, _ := os.ReadFile(path)
|
||||
if !strings.Contains(string(data), strings.Repeat("0", 64)) {
|
||||
t.Fatalf("genesis prev_hash should be 64 zeros, got: %s", data)
|
||||
}
|
||||
}
|
||||
|
||||
func TestVerifyEmptyChain(t *testing.T) {
|
||||
if err := VerifyChain(auditFixture(t)); err != nil {
|
||||
t.Fatalf("empty/missing chain should verify, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTamperDetected(t *testing.T) {
|
||||
path := auditFixture(t)
|
||||
_ = AppendSession(path, sampleSummary("session-a"))
|
||||
_ = AppendSession(path, sampleSummary("session-b"))
|
||||
_ = AppendSession(path, sampleSummary("session-c"))
|
||||
|
||||
// Corrupt the middle line's switch_count.
|
||||
data, _ := os.ReadFile(path)
|
||||
lines := strings.Split(strings.TrimRight(string(data), "\n"), "\n")
|
||||
lines[1] = strings.Replace(lines[1], `"switch_count":3`, `"switch_count":99`, 1)
|
||||
if err := os.WriteFile(path, []byte(strings.Join(lines, "\n")+"\n"), 0o644); err != nil {
|
||||
t.Fatalf("rewrite: %v", err)
|
||||
}
|
||||
err := VerifyChain(path)
|
||||
if err == nil || !strings.Contains(err.Error(), "line 2") {
|
||||
t.Fatalf("tamper on line 2 should be reported, got %v", err)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user