127 lines
4.2 KiB
Go
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
|
|
}
|