Add keylogging functionality to the agent. Implement keylog start/stop, file listing, and download API endpoints. Update web interface to display keystroke logs and allow file downloads.

This commit is contained in:
2026-08-28 12:44:09 +03:00
parent 16d7aa75b3
commit d48c2044bc
13 changed files with 988 additions and 11 deletions
+166
View File
@@ -0,0 +1,166 @@
package keylog
import (
"os"
"path/filepath"
"strings"
"testing"
"time"
)
func TestHourBucket(t *testing.T) {
t.Parallel()
when := time.Date(2026, 8, 28, 13, 45, 0, 0, time.FixedZone("EEST", 3*3600))
if got := HourBucket(when); got != "2026-08-28-13" {
t.Fatalf("HourBucket() = %q, want 2026-08-28-13", got)
}
}
func TestLogFilename(t *testing.T) {
t.Parallel()
if got := LogFilename("2026-08-28-13"); got != "2026-08-28-13.log" {
t.Fatalf("LogFilename() = %q", got)
}
}
func TestValidLogFilename(t *testing.T) {
t.Parallel()
cases := map[string]bool{
"2026-08-28-13.log": true,
"2026-01-01-00.log": true,
"../2026-08-28-13.log": true,
"notes.log": false,
"2026-08-28.log": false,
"2026-08-28-13.txt": false,
"": false,
}
for name, want := range cases {
if got := ValidLogFilename(name); got != want {
t.Fatalf("ValidLogFilename(%q) = %v, want %v", name, got, want)
}
}
}
func TestSectionHeader(t *testing.T) {
t.Parallel()
got := sectionHeader("injected", "Notepad")
want := "[injected · Notepad]\n"
if got != want {
t.Fatalf("sectionHeader() = %q, want %q", got, want)
}
}
func TestWriterTranscript(t *testing.T) {
dir := t.TempDir()
writer, err := NewWriter(dir)
if err != nil {
t.Fatalf("NewWriter: %v", err)
}
t.Cleanup(func() { _ = writer.Close() })
when := time.Date(2026, 8, 28, 13, 0, 0, 0, time.Local)
events := []Event{
{Time: when, Window: "Cursor - main.go", Text: "hello"},
{Time: when, Window: "Cursor - main.go", Text: " world"},
{Time: when, Window: "Notepad", Injected: true, Text: "ai "},
{Time: when, Window: "Notepad", Injected: true, Text: "typed"},
}
for _, event := range events {
if err := writer.Write(event); err != nil {
t.Fatalf("Write: %v", err)
}
}
if err := writer.Close(); err != nil {
t.Fatalf("Close: %v", err)
}
data, err := os.ReadFile(filepath.Join(dir, LogFilename(HourBucket(when))))
if err != nil {
t.Fatalf("read file: %v", err)
}
text := string(data)
wantParts := []string{
"[user · Cursor - main.go]",
"hello world",
"[injected · Notepad]",
"ai typed",
}
for _, part := range wantParts {
if !strings.Contains(text, part) {
t.Fatalf("file = %q, missing %q", text, part)
}
}
if strings.Count(text, "[user · Cursor - main.go]") != 1 {
t.Fatalf("expected one user section header, got %q", text)
}
}
func TestWriterRotation(t *testing.T) {
dir := t.TempDir()
writer, err := NewWriter(dir)
if err != nil {
t.Fatalf("NewWriter: %v", err)
}
t.Cleanup(func() { _ = writer.Close() })
zone := time.FixedZone("EEST", 3*3600)
first := time.Date(2026, 8, 28, 13, 59, 0, 0, zone)
second := time.Date(2026, 8, 28, 14, 0, 0, 0, zone)
if err := writer.Write(Event{Time: first, Window: "Notepad", Text: "a"}); err != nil {
t.Fatalf("Write first: %v", err)
}
if err := writer.Write(Event{Time: second, Window: "Notepad", Text: "b"}); err != nil {
t.Fatalf("Write second: %v", err)
}
if err := writer.Close(); err != nil {
t.Fatalf("Close: %v", err)
}
firstData, err := os.ReadFile(filepath.Join(dir, "2026-08-28-13.log"))
if err != nil {
t.Fatalf("read first file: %v", err)
}
secondData, err := os.ReadFile(filepath.Join(dir, "2026-08-28-14.log"))
if err != nil {
t.Fatalf("read second file: %v", err)
}
if !strings.Contains(string(firstData), "a") {
t.Fatalf("first file = %q", firstData)
}
if !strings.Contains(string(secondData), "b") {
t.Fatalf("second file = %q", secondData)
}
}
func TestPruneOldLogs(t *testing.T) {
dir := t.TempDir()
oldPath := filepath.Join(dir, "2020-01-01-12.log")
newPath := filepath.Join(dir, "2026-08-28-13.log")
if err := os.WriteFile(oldPath, []byte("old"), 0o600); err != nil {
t.Fatalf("write old: %v", err)
}
if err := os.WriteFile(newPath, []byte("new"), 0o600); err != nil {
t.Fatalf("write new: %v", err)
}
oldTime := time.Now().AddDate(0, 0, -30)
if err := os.Chtimes(oldPath, oldTime, oldTime); err != nil {
t.Fatalf("Chtimes old: %v", err)
}
if err := PruneOldLogs(dir, 7); err != nil {
t.Fatalf("PruneOldLogs: %v", err)
}
if _, err := os.Stat(oldPath); !os.IsNotExist(err) {
t.Fatalf("old file still present")
}
if _, err := os.Stat(newPath); err != nil {
t.Fatalf("new file missing: %v", err)
}
}
func TestOpenValidation(t *testing.T) {
if _, _, err := Open("bad-name.log"); err != os.ErrInvalid {
t.Fatalf("Open bad-name.log = %v, want ErrInvalid", err)
}
}