/*
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:
{{range .Triggers}}
- Match: {{.Match}}
{{end}}
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)
}
}