/* Entwickelt von torbenmartin Intention für dieses Projekt: Es lief fail2ban welches ständig ausgelöst hat. Jetzt sollten alle Logs "manuell" überprüft werden ob das Auslösen berechtigt war und ob man den Grund erkennen kann. Da das Logfile schon an einem halben Tag sehr sehr groß wurde war das nicht mehr ohne dieses Projekt möglich. */ /* Example config.json { "legit_paths": [ "/login.php", ], "triggers": [ { "match": ".*HTTP/1.1.*400.*", }, { "match": ".*HTTP/1.1.*401.*", }, { "match": ".*HTTP/1.1.*403.*", } ], "max_retries": 5, "check_minutes": 5 } */ package main import ( "bufio" "encoding/json" "fmt" "html/template" "net/http" "os" "regexp" "sort" "time" "strings" ) // LogEntry enthält die Daten eines einzelnen Logeintrags type LogEntry struct { IP string User string DateTime time.Time Method string Path string Protocol string Status string Size string RawLine string } // Traffik Analysieren und Liste zurück geben. type TrafficEvaluation struct { Legit bool Fail2BanTriggered bool StatusCounts map[string]int CombinedTriggerCount int } // LogData enthält eine Sammlung von Logeinträgen und eine Bewertung type LogData struct { Entries []LogEntry Assessment template.HTML } // PageData enthält die Daten für die HTML-Seite type PageData struct { Now time.Time AppErrorCount int AppCount int AppAttackCount int Logs map[string]LogData Triggers []Trigger MaxRetries int CheckMinutes int } // Counter enthält Zähler für verschiedene Fehlertypen type Counter struct { AppError int AppNormal int AppAttack int } // Konfigurationsstruktur zum Laden der legitimen Pfade type Config struct { LegitPaths []string `json:"legit_paths"` Triggers []Trigger `json:"triggers"` MaxRetries int `json:"max_retries"` CheckMinutes int `json:"check_minutes"` } // Trigger enthält die Details zu einem Fail2Ban-Trigger type Trigger struct { Match string `json:"match"` } var logEntries map[string]LogData var counter Counter var config Config var configLastModified time.Time var trafficEvaluated bool = false var combinedTriggerCount int var tmpl = ` CoreTraffic-Secure-Analytics-Report

CoreTraffic Secure Analytics Report

– powered by torbenmartin -




Report erstellt am {{.Now.Format "02.01.2006 um 15:04:05"}}



Traffic legitim Fail2ban ausgelöst Fail2ban alt ausgelöst Anzahl Ergebnis
true true true {{.AppErrorCount}} Die App hat zu viele Fehler verursacht
false true true {{.AppAttackCount}} Fail2ban hat richtig ausgelöst.
true false true {{.AppCount}} Die Daten, die vorher zum Auslösen geführt haben, sind in alten Logs.


Trigger Matches:

Maximale Versuche: {{.MaxRetries}}

Auslöse-Zeit: {{.CheckMinutes}} Minuten



{{range $ip, $data := .Logs}}

Logs für IP: {{$ip}}

IP              USER   DATE                          METHOD PATH                               PROTO      CODE SIZE
-----------------------------------------------------------------------------------------------
{{range $entry := $data.Entries}}
{{printf "%-15s %-6s [%s] %-5s %-35s %-10s %-4s %s" .IP .User (.DateTime.Format "02/Jan/2006:15:04:05 -0700") .Method .Path .Protocol .Status .Size}}
{{end}}
		

{{$data.Assessment}}


{{end}}

© {{.Now.Format "2006"}} torbenmartin. Alle Rechte vorbehalten.
v.1.2

`; // Funktion zum Laden der Konfiguration aus der Datei func loadConfig() error { fileInfo, err := os.Stat("config.json") if err != nil { return fmt.Errorf("Fehler beim Abrufen der Konfigurationsdatei-Informationen: %v", err) } // Wenn die Datei seit dem letzten Laden geändert wurde if fileInfo.ModTime().After(configLastModified) { // Counter Sperre zurücksetzen trafficEvaluated = false counter.AppAttack = 0 counter.AppNormal = 0 counter.AppError = 0 file, err := os.Open("config.json") if err != nil { return fmt.Errorf("Fehler beim Öffnen der Konfigurationsdatei: %v", err) } defer file.Close() decoder := json.NewDecoder(file) err = decoder.Decode(&config) if err != nil { return fmt.Errorf("Fehler beim Dekodieren der Konfigurationsdatei: %v", err) } // Aktualisiere den Zeitstempel der letzten Änderung configLastModified = fileInfo.ModTime() } return nil } // Funktion zum Parsen einer Log-Zeile func parseLogLine(line string) (*LogEntry, error) { re := regexp.MustCompile(`^(\d+\.\d+\.\d+\.\d+)\s+-\s+(\S+)\s+\[(.*?)\]\s+"(\S+)\s+(\S+)\s+(\S+)"\s+(\d+|\-)\s+(\d+|\-)$`) matches := re.FindStringSubmatch(line) if len(matches) != 9 { return nil, fmt.Errorf("ungültige Zeile: %s", line) } t, err := time.Parse("02/Jan/2006:15:04:05 -0700", matches[3]) if err != nil { return nil, fmt.Errorf("Zeitfehler: %v", err) } return &LogEntry{ IP: matches[1], User: matches[2], DateTime: t, Method: matches[4], Path: matches[5], Protocol: matches[6], Status: matches[7], Size: matches[8], RawLine: line, }, nil } // Funktion zum Einlesen und Gruppieren der Logdaten func readAndGroupLogs(filename string) error { file, err := os.Open(filename) if err != nil { return fmt.Errorf("Fehler beim Öffnen der Datei: %v", err) } defer file.Close() logEntries = make(map[string]LogData) scanner := bufio.NewScanner(file) for scanner.Scan() { line := scanner.Text() if line == "" { continue } entry, err := parseLogLine(line) if err != nil { continue } existing := logEntries[entry.IP] existing.Entries = append(existing.Entries, *entry) logEntries[entry.IP] = existing } for ip, data := range logEntries { sort.Slice(data.Entries, func(i, j int) bool { return data.Entries[i].DateTime.Before(data.Entries[j].DateTime) }) evaluation := evaluateTraffic(data.Entries) assessmentHTML := buildAssessmentHTML(evaluation) logEntries[ip] = LogData{ Entries: data.Entries, Assessment: assessmentHTML, } } // ✅ Analyse abgeschlossen, jetzt Counter Sperre setzen trafficEvaluated = true return nil } func evaluateTraffic(entries []LogEntry) TrafficEvaluation { statusCounts := map[string]int{ "200": 0, "302": 0, "400": 0, "401": 0, "403": 0, "404": 0, } legit := false validPaths := make(map[string]bool) for _, path := range config.LegitPaths { validPaths[path] = true } for _, entry := range entries { statusCounts[entry.Status]++ // Überprüfe, ob der Pfad mit einem der legitimen Pfade übereinstimmt for validPath := range validPaths { if entry.Method == "POST" && entry.Status == "200" && strings.HasPrefix(entry.Path, validPath) { legit = true break // Stoppe, sobald wir einen legitimen Pfad gefunden haben } } } fail2banTriggered := false combinedTriggerCount := 0 var lastTriggerTime time.Time for _, trigger := range config.Triggers { re, err := regexp.Compile(trigger.Match) if err != nil { fmt.Println("Fehler beim Regex:", err) continue } for _, entry := range entries { combined := fmt.Sprintf("%s %s [%s] %s %s %s %s %s", entry.IP, entry.User, entry.DateTime.Format("02/Jan/2006:15:04:05 -0700"), entry.Method, entry.Path, entry.Protocol, entry.Status, entry.Size) if re.MatchString(combined) { // Prüfen, ob der Unterschied in der Zeit größer als check_minutes ist if !lastTriggerTime.IsZero() && entry.DateTime.Sub(lastTriggerTime).Minutes() > float64(config.CheckMinutes) { // Wenn die Zeitspanne mehr als check_minutes beträgt, zählen wir den Treffer als neuen combinedTriggerCount = 1 // Zählt den aktuellen Treffer als neuen } else { combinedTriggerCount++ // Zählt den Treffer innerhalb des Zeitrahmens } lastTriggerTime = entry.DateTime // Speichert die Zeit des letzten Treffers } } } if combinedTriggerCount > config.MaxRetries { fail2banTriggered = true } return TrafficEvaluation{ Legit: legit, Fail2BanTriggered: fail2banTriggered, StatusCounts: statusCounts, CombinedTriggerCount: combinedTriggerCount, } } func buildAssessmentHTML(eval TrafficEvaluation) template.HTML { var trafficAssessment, fail2banWarning, appErrorAssessment string if eval.Legit { trafficAssessment = "Legitimer Traffic" } else { trafficAssessment = "Verdächtiger Traffic" } if eval.Fail2BanTriggered { fail2banWarning = fmt.Sprintf(`Warnung: %d Treffer – Fail2ban hat hier ausgelöst.`, eval.CombinedTriggerCount) } else { fail2banWarning = "Fail2Ban hat hier nicht ausgelöst" } if eval.Legit { if !eval.Fail2BanTriggered { if !trafficEvaluated { counter.AppNormal++ } appErrorAssessment = "Traffic legitim - Fail2Ban ausgelöst. Sollte es aber nicht. Grund ist in alten Logs zu finden." } else { if !trafficEvaluated { counter.AppError++ } appErrorAssessment = "Traffic legitim - Fail2Ban hat ausgelöst (App-Fehler)" } } else { if !eval.Fail2BanTriggered { if !trafficEvaluated { counter.AppAttack++ counter.AppNormal++ } appErrorAssessment = "Traffic nicht legitim - Fail2Ban hat hier nicht ausgelöst. Grund ist in alten Logs zu finden." } else { if !trafficEvaluated { counter.AppAttack++ } appErrorAssessment = "Traffic nicht legitim - Fail2Ban hat ausgelöst (Berechtigt)" } } assessment := fmt.Sprintf( `Statuscode-Zählungen:
200: %d
302: %d
400: %d
401: %d
403: %d
404: %d

1. Bewertung der Aufrufe:
%s

2. Bewertung für Fail2Ban:
%s

3. Bewertung:
%s
`, eval.StatusCounts["200"], eval.StatusCounts["302"], eval.StatusCounts["400"], eval.StatusCounts["401"], eval.StatusCounts["403"], eval.StatusCounts["404"], trafficAssessment, fail2banWarning, appErrorAssessment, ) return template.HTML(assessment) } // Handler für die Web-Seite func handler(w http.ResponseWriter, r *http.Request) { // Konfiguration bei jedem Anfrage nur neu laden, wenn geändert err := loadConfig() if err != nil { http.Error(w, "Fehler beim Laden der Konfiguration: "+err.Error(), http.StatusInternalServerError) return } // Lese und gruppiere Logs nach jeder Anfrage err = readAndGroupLogs("access.log") if err != nil { http.Error(w, "Fehler beim Einlesen des Logfiles: "+err.Error(), http.StatusInternalServerError) return } // Template rendern t := template.Must(template.New("log").Parse(tmpl)) page := PageData{ Now: time.Now(), AppErrorCount: counter.AppError, AppCount: counter.AppNormal, AppAttackCount: counter.AppAttack, Logs: logEntries, Triggers: config.Triggers, MaxRetries: config.MaxRetries, CheckMinutes: config.CheckMinutes, } err = t.Execute(w, page) if err != nil { http.Error(w, "Template Fehler: "+err.Error(), http.StatusInternalServerError) return } } // Main-Funktion zum Starten des Servers func main() { // Starte den HTTP-Server http.HandleFunc("/", handler) fmt.Println("Server läuft auf http://localhost:8081") err := http.ListenAndServe(":8081", nil) if err != nil { fmt.Println("Fehler beim Starten des Servers:", err) } }