package audit import ( "bufio" "fmt" "os/exec" "regexp" "time" ) func now() time.Time { return time.Now() } var ( reAccepted = regexp.MustCompile(`Accepted publickey for (\S+) from (\S+) port \d+`) reDisconnect = regexp.MustCompile(`Disconnected from user (\S+) (\S+) port \d+`) reSessClosed = regexp.MustCompile(`session closed for user (\S+)`) ) // TailJournal runs indefinitely (intended to be run under systemd — see // scripts/ssh-access-tui-audit.service), following sshd's journal output and // appending normalized connect/disconnect events to the audit log at // logPath. This is what makes "who logged in/out" survive independently of // journald's own retention/rotation policy. func TailJournal(logPath string) error { cmd := exec.Command("journalctl", "-u", "ssh", "-u", "sshd", "-f", "-n", "0", "-o", "cat") stdout, err := cmd.StdoutPipe() if err != nil { return fmt.Errorf("journalctl pipe: %w", err) } if err := cmd.Start(); err != nil { return fmt.Errorf("start journalctl: %w", err) } sc := bufio.NewScanner(stdout) for sc.Scan() { line := sc.Text() switch { case reAccepted.MatchString(line): m := reAccepted.FindStringSubmatch(line) _ = Append(logPath, Event{Time: now(), User: m[1], Action: "connect", SrcIP: m[2]}) case reDisconnect.MatchString(line): m := reDisconnect.FindStringSubmatch(line) _ = Append(logPath, Event{Time: now(), User: m[1], Action: "disconnect", SrcIP: m[2]}) case reSessClosed.MatchString(line): m := reSessClosed.FindStringSubmatch(line) _ = Append(logPath, Event{Time: now(), User: m[1], Action: "disconnect"}) } } return cmd.Wait() }