go-wiki/handler/audit.go
2026-07-14 20:46:19 +02:00

127 lines
4.2 KiB
Go

// 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
}