// Package audit maintains an append-only record of connect/disconnect // events (independent of journald, which may rotate) and answers "who is // online right now" — which for jump/forward-only sessions is not visible // via `who`/`loginctl`, since those rely on pty allocation that this tool // deliberately disables. package audit import ( "bufio" "encoding/json" "fmt" "os" "time" "github.com/gofrs/flock" ) // Event is one line of the audit trail. type Event struct { Time time.Time `json:"time"` User string `json:"user"` Action string `json:"action"` // "connect", "disconnect", "admin" Detail string `json:"detail,omitempty"` SrcIP string `json:"src_ip,omitempty"` } // Append adds one event to the JSONL audit log at path, under a file lock // so the interactive TUI and the background journal-tailer (see tail.go) // can write concurrently without interleaving corrupt lines. func Append(path string, ev Event) error { fl := flock.New(path + ".lock") if err := fl.Lock(); err != nil { return fmt.Errorf("lock audit log: %w", err) } defer fl.Unlock() f, err := os.OpenFile(path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o640) if err != nil { return fmt.Errorf("open audit log: %w", err) } defer f.Close() data, err := json.Marshal(ev) if err != nil { return err } _, err = f.Write(append(data, '\n')) return err } // ReadRecent returns the last n events from the audit log, oldest first. func ReadRecent(path string, n int) ([]Event, error) { f, err := os.Open(path) if os.IsNotExist(err) { return nil, nil } if err != nil { return nil, err } defer f.Close() var all []Event sc := bufio.NewScanner(f) sc.Buffer(make([]byte, 64*1024), 1024*1024) for sc.Scan() { var ev Event if err := json.Unmarshal(sc.Bytes(), &ev); err == nil { all = append(all, ev) } } if len(all) > n { all = all[len(all)-n:] } return all, sc.Err() }