// Dieses File implementiert das Audit-Log: ein Nachvollzieh-Protokoll für // sicherheits-/nachvollziehbarkeitsrelevante Schreib-Aktionen (Upload, Delete, // Login-Versuche, Webhook-Sync). // // Format: JSON Lines (eine JSON-Zeile pro Ereignis, append-only) — passt zum // datei-zentrierten Charakter des restlichen Projekts, lässt sich ohne eigenes // Tooling mit "tail -f" und "jq" auswerten, und braucht keine Datenbank. // // Eine einzige Datei für alle Teams (nicht pro Team) hält Schreiben/Locking // einfach — beim Anzeigen (siehe audit_page.go) wird bei Bedarf nach Team gefiltert. package handler import ( "bufio" "encoding/json" "log" "os" "sync" "time" ) // AuditEvent ist ein einzelner Eintrag im Audit-Log. type AuditEvent struct { Time time.Time `json:"time"` Team string `json:"team"` // Team-ID; leer bei teamübergreifenden Events (z.B. webhook_sync) oder fehlgeschlagenem Login Actor string `json:"actor"` // "team" oder "admin" — wer hat gehandelt Action string `json:"action"` // z.B. "upload", "delete", "login_success", "login_failure", "webhook_sync" Path string `json:"path,omitempty"` // betroffener Seiten-Pfad, falls zutreffend Source string `json:"source"` // "web", "api", "mcp", "webhook" IP string `json:"ip,omitempty"` // Client-IP, falls bekannt (z.B. bei Login) Detail string `json:"detail,omitempty"` // zusätzlicher Kontext, z.B. eine Fehlermeldung } // AuditLogger schreibt AuditEvents als JSON Lines in eine Datei und kann die // zuletzt geschriebenen Einträge wieder auslesen (für die Admin-Ansicht). type AuditLogger struct { mu sync.Mutex path string } // NewAuditLogger erstellt einen AuditLogger der in die angegebene Datei schreibt. // Die Datei wird beim ersten Log-Aufruf automatisch angelegt falls sie nicht existiert. func NewAuditLogger(path string) *AuditLogger { return &AuditLogger{path: path} } // Log hängt ein Ereignis ans Ende der Audit-Log-Datei an. // // Ein Fehler beim Schreiben des Audit-Logs lässt die eigentliche Aktion (Upload, // Login, ...) nicht fehlschlagen — er wird nur geloggt. Das Audit-Log ist eine // Nachvollziehbarkeits-Hilfe, kein Teil des kritischen Pfads. func (a *AuditLogger) Log(event AuditEvent) { event.Time = time.Now() line, err := json.Marshal(event) if err != nil { log.Printf("Audit-Log: Ereignis konnte nicht serialisiert werden: %v", err) return } a.mu.Lock() defer a.mu.Unlock() f, err := os.OpenFile(a.path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) if err != nil { log.Printf("Audit-Log: Datei konnte nicht geöffnet werden: %v", err) return } defer f.Close() if _, err := f.Write(append(line, '\n')); err != nil { log.Printf("Audit-Log: Ereignis konnte nicht geschrieben werden: %v", err) } } // Tail liest die letzten n Ereignisse aus der Audit-Log-Datei, neueste zuerst. // Existiert die Datei noch nicht (noch kein Ereignis protokolliert), wird eine // leere Liste ohne Fehler zurückgegeben. func (a *AuditLogger) Tail(n int) ([]AuditEvent, error) { a.mu.Lock() defer a.mu.Unlock() f, err := os.Open(a.path) if os.IsNotExist(err) { return nil, nil } if err != nil { return nil, err } defer f.Close() var lines []string scanner := bufio.NewScanner(f) // Der Standard-Puffer von bufio.Scanner ist auf 64 KB pro Zeile begrenzt — // für unsere kurzen JSON-Zeilen reichlich, aber sicherheitshalber explizit // auf 1 MB angehoben falls ein Detail-Feld mal ungewöhnlich lang wird. scanner.Buffer(make([]byte, 0, 64*1024), 1024*1024) for scanner.Scan() { line := scanner.Text() if line != "" { lines = append(lines, line) } } if err := scanner.Err(); err != nil { return nil, err } if len(lines) > n { lines = lines[len(lines)-n:] } events := make([]AuditEvent, 0, len(lines)) for _, line := range lines { var event AuditEvent if err := json.Unmarshal([]byte(line), &event); err != nil { continue // defekte Zeile überspringen statt die ganze Anzeige abbrechen zu lassen } events = append(events, event) } // Neueste zuerst anzeigen. for i, j := 0, len(events)-1; i < j; i, j = i+1, j-1 { events[i], events[j] = events[j], events[i] } return events, nil }