diff --git a/cli/cmd_vault.go b/cli/cmd_vault.go index 84ba5dd9..55ed4030 100644 --- a/cli/cmd_vault.go +++ b/cli/cmd_vault.go @@ -182,6 +182,39 @@ func terminalAllowed(term, termProgram string) bool { return false } +// opRecord is one CLI operation. ItemName is accepted for the caller's +// convenience but is INTENTIONALLY never rendered into the log line — auditing +// which of your own logins you opened is itself sensitive, and per-item reads +// are invisible server-side anyway (spec §9a). +type opRecord struct { + User string + Verb string + PID int + PPID int + ParentComm string + ItemName string // never logged +} + +func opLogLine(r opRecord) string { + return fmt.Sprintf("user=%s verb=%s pid=%d ppid=%d parent=%s", + r.User, r.Verb, r.PID, r.PPID, r.ParentComm) +} + +// parentComm reads /proc//comm (best-effort; "" on failure). +func parentComm(ppid int) string { + b, err := os.ReadFile(fmt.Sprintf("/proc/%d/comm", ppid)) + if err != nil { + return "" + } + return strings.TrimSpace(string(b)) +} + +// writeOpLog appends one privacy-aware line to the user's op-log (best-effort; +// never blocks or fails the command). Goes to syslog so it ships to Loki. +func writeOpLog(r opRecord) { + exec.Command("logger", "-t", "homelab-vault", opLogLine(r)).Run() // best-effort +} + func vaultSetup(args []string) error { return fmt.Errorf("not implemented") } func vaultStatus(args []string) error { return fmt.Errorf("not implemented") } func vaultList(args []string) error { return fmt.Errorf("not implemented") } diff --git a/cli/cmd_vault_test.go b/cli/cmd_vault_test.go index 30fd540d..b3a20ef4 100644 --- a/cli/cmd_vault_test.go +++ b/cli/cmd_vault_test.go @@ -176,3 +176,17 @@ func TestTerminalAllowed(t *testing.T) { } } } + +func TestOpLogLineHasNoSecretOrItem(t *testing.T) { + line := opLogLine(opRecord{User: "emo", Verb: "get", PID: 10, PPID: 9, ParentComm: "claude", ItemName: "Chase Bank"}) + for _, must := range []string{"user=emo", "verb=get", "ppid=9", "parent=claude"} { + if !strings.Contains(line, must) { + t.Errorf("op-log missing %q: %s", must, line) + } + } + for _, mustNot := range []string{"Chase", "password", "secret"} { + if strings.Contains(line, mustNot) { + t.Fatalf("op-log LEAKS %q (privacy violation): %s", mustNot, line) + } + } +}