package security import ( "bufio" "encoding/json" "os" "path/filepath" "strings" "testing" ) func readAuditLines(t *testing.T, path string) []AuditEvent { t.Helper() f, err := os.Open(path) if err != nil { t.Fatalf("open audit log: %v", err) } defer f.Close() var events []AuditEvent sc := bufio.NewScanner(f) for sc.Scan() { var ev AuditEvent if err := json.Unmarshal(sc.Bytes(), &ev); err != nil { t.Fatalf("decode line %q: %v", sc.Text(), err) } events = append(events, ev) } if err := sc.Err(); err != nil { t.Fatalf("scan audit log: %v", err) } return events } func TestAuditLogger_NilReceiverIsNoop(t *testing.T) { var a *AuditLogger // Must not panic. a.Record(AuditEvent{Action: "block"}) } func TestAuditLogger_DisabledWhenPathEmpty(t *testing.T) { a := NewAuditLogger(AuditLoggerConfig{}) if a != nil { t.Errorf("expected nil logger for empty path, got %v", a) } } func TestAuditLogger_AppendsJSONLines(t *testing.T) { dir := t.TempDir() path := filepath.Join(dir, "audit.jsonl") a := NewAuditLogger(AuditLoggerConfig{Path: path}) if a == nil { t.Fatal("expected non-nil logger") } a.Record(AuditEvent{Action: "block", Pattern: "anthropic_api_key", Source: "tool_result", TokenLen: 51}) a.Record(AuditEvent{Action: "redact", Pattern: "high_entropy", Source: "message_text", TokenLen: 42}) events := readAuditLines(t, path) if len(events) != 2 { t.Fatalf("expected 2 events, got %d", len(events)) } if events[0].Action != "block" || events[0].Pattern != "anthropic_api_key" { t.Errorf("event 0 = %+v", events[0]) } if events[0].Timestamp.IsZero() { t.Error("event 0 missing timestamp") } if events[1].Action != "redact" || events[1].TokenLen != 42 { t.Errorf("event 1 = %+v", events[1]) } } func TestAuditLogger_SkipsUnderIncognito(t *testing.T) { dir := t.TempDir() path := filepath.Join(dir, "audit.jsonl") incog := NewIncognitoMode() a := NewAuditLogger(AuditLoggerConfig{Path: path, Incognito: incog}) incog.Activate() a.Record(AuditEvent{Action: "block", Pattern: "x"}) if _, err := os.Stat(path); !os.IsNotExist(err) { t.Errorf("expected audit file to not exist under incognito, got err=%v", err) } incog.Deactivate() a.Record(AuditEvent{Action: "block", Pattern: "y"}) events := readAuditLines(t, path) if len(events) != 1 { t.Fatalf("expected 1 event after deactivate, got %d", len(events)) } if events[0].Pattern != "y" { t.Errorf("expected pattern=y (incognito event dropped), got %q", events[0].Pattern) } } func TestAuditLogger_CreatesParentDir(t *testing.T) { dir := t.TempDir() path := filepath.Join(dir, "deeply", "nested", "audit.jsonl") a := NewAuditLogger(AuditLoggerConfig{Path: path}) a.Record(AuditEvent{Action: "block"}) if _, err := os.Stat(path); err != nil { t.Errorf("expected audit file at %s, got err=%v", path, err) } } func TestFirewall_RecordsRedactionToAudit(t *testing.T) { dir := t.TempDir() auditPath := filepath.Join(dir, "audit.jsonl") audit := NewAuditLogger(AuditLoggerConfig{Path: auditPath}) fw := NewFirewall(FirewallConfig{ ScanOutgoing: true, ScanToolResults: true, Audit: audit, }) // Anthropic key prefix is a built-in redact pattern; emit it // through the tool-result scanning path. cleaned := fw.ScanToolResult("here is the key sk-ant-abcdef1234567890abcdef1234567890abcdef") if !strings.Contains(cleaned, "[REDACTED]") { t.Errorf("expected [REDACTED] in cleaned content, got %q", cleaned) } events := readAuditLines(t, auditPath) var sawAnthropicRedact bool for _, ev := range events { if ev.Action == "redact" && ev.Pattern == "anthropic_api_key" && ev.Source == "tool_result" { sawAnthropicRedact = true if ev.TokenLen == 0 { t.Errorf("expected non-zero TokenLen on redact event, got %+v", ev) } } } if !sawAnthropicRedact { t.Errorf("expected an anthropic_api_key redact event in audit log, got %+v", events) } }