/* Copyright (c) 2025 Mario Stöckl (mstoeck3@hs-mittweida.de). Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ // Abhängigkeiten: // zlib, gcc (statisch: glibc-static, zlib-static) // KOMPILIERUNG: // gcc -o bin/main src/main.c lib/gzunpack.c -Ilib -lz (statisch: gcc -static -o bin/main src/main.c lib/gzunpack.c -Ilib -lz) // ABHÄNGIGKEITEN: // gcc, zlib-devel #include #include #include #include // library für Interaktion mit Ordnerstrukturen #include // library für is_directory: Unterscheidung zwischen Dateien und Ordnern #include // um aktuelle Zeit zu generieren #include "gzunpack.h" #define INITIAL_ENTRIES 1000 // globale Variable zur initialen Speicherallokation in allocate_initial_memory(). Wird falls nötig um GROWTH_FACTOR erweitert #define GROWTH_FACTOR 1.1 // wird in mem_expand_dynamically() genutzt, um den Speicher zu vergrößern #define MAX_FILTERS 100 #define MAX_REQUEST_LENGTH 8192 // das hohe Limit ist erforderlich, da teilweise ausufernde JSON-Requests in nginx auflaufen können. #define TOP_X 20 // definiert die Anzahl der Einträge, die in den Top-Listen angezeigt werden sollen #define SUSPICIOUS_REQUEST_LEN_THRESHOLD 256 // struct für die Darstellung von Timestamps. Die granulare Trennung in verschiedene int-Werte macht die spätere Verarbeitung modular anpassbar/erweiterbar und erleichtert die Verarbeitung. struct simple_time_t { int day; int month; int year; int hour; int minute; int second; }; // Struktur für die Darstellung eines Standard-NGINX-Logeintrags. struct log_entry_t { char ip_address[50]; // ausreichende Längenbegrenzung für IP-Adressen. Könnte theoretisch auch ipv6 (ungetestet) char request_method[10]; // GET, POST, PUT, DELETE, PROPFIND ... char url_path[MAX_REQUEST_LENGTH]; // Pfade können lang werden, insbesodere bei base64-Strings wie oft in Malware verwendet int status_code; int bytes_sent; struct simple_time_t time; char referrer[128]; char user_agent[256]; char source_file[256]; char parsing_timestamp[32]; int annotated_flag; char annotation[64]; }; // Struktur für einen Status-Filtereintrag mit Inhalt & Modus struct status_filter_t { int code; int filter_exclude_flag; }; struct method_filter_t { char pattern[10]; int filter_exclude_flag; }; // für IP-Adressen struct ip_filter_t { char ip_address[50]; int filter_exclude_flag; }; // für Zeit, Start- und Endzeit struct time_filter_t { struct simple_time_t start_time; struct simple_time_t end_time; int filter_exclude_flag; }; // Filter für User-Agent struct user_agent_filter_t { char pattern[256]; int filter_exclude_flag; }; // Filter für URL-Pfad/Request struct url_filter_t { char pattern[MAX_REQUEST_LENGTH]; int filter_exclude_flag; }; struct annotation_flag_filter_t { int annotation_flag_is_present; int filter_exclude_flag; }; struct annotation_filter_t { char pattern[64]; int filter_exclude_flag; }; // Struktur zum erhalten aller Filtereinträge, kann im Dialogbetrieb bearbeitet werden. Mit Zähler. struct filter_system_t { struct status_filter_t status_filters[MAX_FILTERS]; int status_count; struct method_filter_t method_filters[MAX_FILTERS]; int method_count; struct ip_filter_t ip_filters[MAX_FILTERS]; int ip_count; struct time_filter_t time_filters[MAX_FILTERS]; int time_count; struct user_agent_filter_t user_agent_filters[MAX_FILTERS]; int user_agent_count; struct url_filter_t url_filters[MAX_FILTERS]; int url_count; struct annotation_flag_filter_t annotation_flag_filter; int annotation_flag_filter_enabled; struct annotation_filter_t annotation_filters[MAX_FILTERS]; int annotation_count; }; // Definition einer Datenstruktur für die IP-Adressen Topliste struct ip_stat_t { char ip_address[50]; int count; }; // Initialisierung eines Arrays für die Logeinträge und weiterer Startvariablen struct log_entry_t *all_entries = NULL; int max_entries = 0; int total_entries = 0; int suspicious_patterns_count = 0; struct filter_system_t filters = {0}; // für -v option int flag_verbose = 0; // Hilfsfunktion für die Erkennung von Leerzeichen int is_space(char c) { return (c == ' ' || c == '\t'); } //Hilfsfunktion zum Prüfen, ob ein String mit einem bestimmten Präfix beginnt. Nötig für das Parsen der Filter in der main()-Funktion int starts_with(char* str, char* prefix) { //positiver Vergleich gibt bei strncmp 0 aus - daher prüfen ob == 0, um 1 bei positiver Prüfung zu erhalten // strncmp statt strcmp, da nur die ersten Bytes beider Strings geprüft werden (Anzahl: drittes Argument) return strncmp(str, prefix, strlen(prefix)) == 0; } // Hilfsfunktion zum Überspringen von Leerzeichen, gibt den Pointer für das nächste nicht-Leerzeichen zurück. Nötig für Parser. char* skip_spaces(char* str) { while (is_space(*str)) { str++; } return str; } // Kopiert einen Eingabestring von einem Quellbereich zu einem Zielbereich, bis ein Leerzeichen (oder Nullterminator) erreicht wird oder die max. Zeilenlänge nicht überschritten wird void copy_until_space(char* destination, char* source, int max_length) { int i = 0; while (source[i] != ' ' && source[i] != '\0' && i < max_length - 1) { destination[i] = source[i]; i++; } destination[i] = '\0'; } // NGINX speichert Timestamps mit Monatskürzel. Umwandlung in Zahlen für maschinelle Verarbeitung. int month_name_to_number(char* month_name) { if (strncmp(month_name, "Jan", 3) == 0) return 1; if (strncmp(month_name, "Feb", 3) == 0) return 2; if (strncmp(month_name, "Mar", 3) == 0) return 3; if (strncmp(month_name, "Apr", 3) == 0) return 4; if (strncmp(month_name, "May", 3) == 0) return 5; if (strncmp(month_name, "Jun", 3) == 0) return 6; if (strncmp(month_name, "Jul", 3) == 0) return 7; if (strncmp(month_name, "Aug", 3) == 0) return 8; if (strncmp(month_name, "Sep", 3) == 0) return 9; if (strncmp(month_name, "Oct", 3) == 0) return 10; if (strncmp(month_name, "Nov", 3) == 0) return 11; if (strncmp(month_name, "Dec", 3) == 0) return 12; return 1; } // Vergleich von Zeitstempel-Strukturen - time_checkvalue ist der Prüfwert, time_filtervalue ist der Vergleichswert (Filter) - Funktion wird in time_matches() aufgerufen int compare_times(struct simple_time_t time_checkvalue, struct simple_time_t time_filtervalue) { /* Rückgabewert 1 -> Prüfwert ist NACH dem Vergleichswert Es wird erst das Jahr geprüft, dann der Monat u.s.w. Ist ein Wert gleich, wird der nächstfeinere Wert geprüft. Sobald die Werte sich unterscheiden, findet die Prüfung statt und ein Rückgabewert wird ausgegeben. Gibt 0 zurück, wenn der Wert dem Filter exakt gleicht */ if (time_checkvalue.year != time_filtervalue.year) return (time_checkvalue.year < time_filtervalue.year) ? -1 : 1; if (time_checkvalue.month != time_filtervalue.month) return (time_checkvalue.month < time_filtervalue.month) ? -1 : 1; if (time_checkvalue.day != time_filtervalue.day) return (time_checkvalue.day < time_filtervalue.day) ? -1 : 1; if (time_checkvalue.hour != time_filtervalue.hour) return (time_checkvalue.hour < time_filtervalue.hour) ? -1 : 1; if (time_checkvalue.minute != time_filtervalue.minute) return (time_checkvalue.minute < time_filtervalue.minute) ? -1 : 1; if (time_checkvalue.second != time_filtervalue.second) return (time_checkvalue.second < time_filtervalue.second) ? -1 : 1; return 0; } // Standardfunktion zum Leeren des Input-Buffers void clear_input_buffer(){ int c; while ((c = getchar()) != '\n' && c != EOF) { } } // Hilfsfunktion zum Prüfen, ob die Zahl eine Dezimalzahl ist. Wird im Menu verwendet, um die Nutzereingabe zu prüfen int read_safe_integer(){ int number; int result = scanf("%d", &number); if (result != 1) { clear_input_buffer(); return -1; // Fehler, wenn die Nutzereingabe kein Integer ist } clear_input_buffer(); return number; } // Speicher freigeben und mit 0 überschreiben (Prävention von use-after-free-Schwachstelle) void cleanup_memory(){ if (all_entries != NULL) { printf("\nDEBUG: %lu Bytes Speicher werden freigegeben\n", (unsigned long)(max_entries * sizeof(struct log_entry_t))); free(all_entries); all_entries = NULL; } max_entries = 0; total_entries = 0; } // sauberes Schließen und bereinigen bei Fehlerstatus, sofern Speicher nicht alloziert werden kann void cleanup_and_exit(){ if (flag_verbose) printf("DEBUG: Programmende. Speicher wird freigegeben und mit NULL überschrieben.\n"); cleanup_memory(); exit(1); } // Erweiterung des Speichers für dynamische Speicherallokation void mem_expand_dynamically(){ // total_entries werden beim parsen am Anfang gezählt (load_regular_file()), max_entries werden initial festgelegt if (total_entries >= max_entries) { int old_max = max_entries; max_entries = max_entries * GROWTH_FACTOR; if (flag_verbose) printf("DEBUG: Dynamische Speichererweiterung von %d auf %d Einträge um Faktor %f\n", old_max, max_entries, GROWTH_FACTOR); struct log_entry_t *new_ptr = realloc(all_entries, max_entries * sizeof(struct log_entry_t)); if (new_ptr == NULL) { printf("ERROR: Speicher konnte nicht auf %d Einträge erweitert werden, ..\n", max_entries); printf("ERROR: Benötigter Speicher: %lu Bytes\n", (unsigned long)(max_entries * sizeof(struct log_entry_t))); cleanup_and_exit(); } all_entries = new_ptr; if (flag_verbose) printf("DEBUG: Speicher erfolgreich erweitert auf %lu Bytes\n", (unsigned long)(max_entries * sizeof(struct log_entry_t))); } } void allocate_initial_memory(){ max_entries = INITIAL_ENTRIES; // Startwert 1000, globale Variable all_entries = malloc(max_entries * sizeof(struct log_entry_t)); if (all_entries == NULL) { printf("ERROR: Konnte %d Einträge nicht allozieren, ..\n", max_entries); printf("ERROR: %lu Bytes\n", (unsigned long)(max_entries * sizeof(struct log_entry_t))); exit(1); // cleanup_and_exit() nicht nötig, da der Speicherbereich nicht beschrieben wurde - use-after-free unproblematisch } if (flag_verbose) printf("DEBUG: Speicher erfolgreich alloziert für %d Log-Einträge (%lu Bytes)\n", max_entries, (unsigned long)(max_entries * sizeof(struct log_entry_t))); } void get_current_timestamp(char* buffer, int buffer_size) { time_t raw_time; struct tm *time_info; time(&raw_time); time_info = localtime(&raw_time); if (time_info != NULL) { strftime(buffer, buffer_size, "%Y-%m-%d %H:%M:%S", time_info); } else { snprintf(buffer, buffer_size, "UNKNOWN"); } } // Hilfsfunktion zum Prüfen, ob es sich beim Pfad um ein Directory handelt - für rekursives Parsen int is_directory(char* path) { struct stat path_stat; if (stat(path, &path_stat) != 0) { return 0; } return S_ISDIR(path_stat.st_mode); } // Hilfsfunktion zum prüfen, ob es sich um eine plausible nginx-Logdatei handelt (Metrik: Dateiname - BESSER: Regex oder Magic Bytes?) int is_log_file(char* filename) { // versteckte Dateien sowie . oder .. überspringen if (filename[0] == '.') { return 0; } // Mögliche Bestandteile von NGINX-Logfile-Dateinamen char* log_patterns[] = { ".log", "access", "error", "combined", "redirect", NULL }; // Vergleich der vorhandenen Dateinamen mit den Suchmustern for (int i = 0; log_patterns[i] != NULL; i++) { if (strstr(filename, log_patterns[i]) != NULL) { if ((strstr(filename, "error")!= NULL)||(strstr(filename, ".gz")!= NULL)) continue; return 1; } } return 0; } // Funktion zum suchen eines Suchbegriffs innerhalb eines Strings (lowercase) int search_in_string(char* raw_string, char* search_string) { char raw_string_lower[512]; // Puffer zum Speichern des zu durchsuchenden Strings char search_string_lower[256]; // Puffer zum Speichern des Suchbegriffs // Konvertierung des Datensatzes zu Kleinbuchstaben int i = 0; // für jeden Buchstaben innerhalb des Datensatzes Verschiebung innerhalb des ASCII-Alphabets um 32 while (raw_string[i] && i < 511) { if (raw_string[i] >= 'A' && raw_string[i] <= 'Z') { raw_string_lower[i] = raw_string[i] + 32; // Verschiebung im ASCII-Alphabet } else { raw_string_lower[i] = raw_string[i]; // alles was kein Buchstabe ist, wird beibehalten } i++; } raw_string_lower[i] = '\0'; // Nullterminator anfügen // gleiche Methode mit dem Suchbegriff i = 0; while (search_string[i] && i < 255) { if (search_string[i] >= 'A' && search_string[i] <= 'Z') { search_string_lower[i] = search_string[i] + 32; // Verschiebung im ASCII-Alphabet } else { search_string_lower[i] = search_string[i]; // nicht-Buchstaben beibehalten } i++; } search_string_lower[i] = '\0'; // Nullterminator anfügen // strstr()-Vergleich - gibt NULL zurück wenn nichts gefunden char* result = strstr(raw_string_lower, search_string_lower); // Einfache Rückgabe: 1 wenn gefunden, 0 wenn nicht gefunden if (result != NULL) { return 1; } else { return 0; } } // Fügt eine Annotation zu einem bestimmten Eintrag hinzu // https://stackoverflow.com/questions/5901181/c-string-append void annotate_entry(int index, char* annotation_string) { if (index >= 0 && index < total_entries) { if (all_entries[index].annotation[0] == '\0'){ strncpy(all_entries[index].annotation, annotation_string, sizeof(all_entries[index].annotation) - 1); all_entries[index].annotation[sizeof(all_entries[index].annotation) - 1] = '\0'; all_entries[index].annotated_flag = 1; } else { char * new_str ; if((new_str = malloc(strlen(all_entries[index].annotation)+strlen(",")+strlen(annotation_string)+1)) != NULL){ new_str[0] = '\0'; // ensures the memory is an empty string strcat(new_str, all_entries[index].annotation); strcat(new_str, ","); strcat(new_str, annotation_string); strncpy(all_entries[index].annotation, new_str, sizeof(all_entries[index].annotation) - 1); all_entries[index].annotation[sizeof(all_entries[index].annotation) - 1] = '\0'; free(new_str); new_str=NULL; } } } } // TRANSPARENZ: Diese Funktion ist KI-generiert void annotate_suspicious_entries(struct log_entry_t* dataset) { if (flag_verbose) printf("DEBUG: Prüfe %d Einträge auf verdächtige Muster...\n", total_entries); for (int i = 0; i < total_entries; i++) { // Initialisierung der Annotation falls noch nicht gesetzt if (all_entries[i].annotation[0] == '\0') { all_entries[i].annotation[0] = '\0'; } // 1. PAYLOAD-GRÖßE: Sehr lange Requests int url_length = strlen(all_entries[i].url_path); if (url_length > SUSPICIOUS_REQUEST_LEN_THRESHOLD) { annotate_entry(i, "Long Payload"); suspicious_patterns_count++; } // 2. PFAD-BASIERTE ANGRIFFE: Häufige Angriffsziele und sensible Pfade // Git-Repository Zugriffe (häufig bei Recon) if (search_in_string(all_entries[i].url_path, ".git/") || search_in_string(all_entries[i].url_path, "/.git")) { annotate_entry(i, "Git Access"); suspicious_patterns_count++; } // Environment-Dateien (kritische Konfigurationsdateien) if (search_in_string(all_entries[i].url_path, ".env") || search_in_string(all_entries[i].url_path, ".config") || search_in_string(all_entries[i].url_path, "config.php")) { annotate_entry(i, "Config Access"); suspicious_patterns_count++; } // WordPress Admin-Bereiche (häufig attackiert) if (search_in_string(all_entries[i].url_path, "wp-admin") || search_in_string(all_entries[i].url_path, "wp-login") || search_in_string(all_entries[i].url_path, "wp-config")) { annotate_entry(i, "WP Attack"); suspicious_patterns_count++; } // Database Management Tools if (search_in_string(all_entries[i].url_path, "phpmyadmin") || search_in_string(all_entries[i].url_path, "phpMyAdmin") || search_in_string(all_entries[i].url_path, "adminer")) { annotate_entry(i, "DB Tool Access"); suspicious_patterns_count++; } // Admin/Management Interfaces if (search_in_string(all_entries[i].url_path, "/admin") || search_in_string(all_entries[i].url_path, "/manager") || search_in_string(all_entries[i].url_path, "/console")) { annotate_entry(i, "Admin Access"); suspicious_patterns_count++; } // 3. DIRECTORY TRAVERSAL: Pfad-Traversal Versuche if (search_in_string(all_entries[i].url_path, "../") || search_in_string(all_entries[i].url_path, "..\\") || search_in_string(all_entries[i].url_path, "%2e%2e%2f") || search_in_string(all_entries[i].url_path, "%2e%2e%5c")) { annotate_entry(i, "Path Traversal"); suspicious_patterns_count++; } // 4. SQL INJECTION: SQL-Keywords in URLs if (search_in_string(all_entries[i].url_path, "select%20") || search_in_string(all_entries[i].url_path, "union%20") || search_in_string(all_entries[i].url_path, "insert%20") || search_in_string(all_entries[i].url_path, "delete%20") || search_in_string(all_entries[i].url_path, "drop%20") || search_in_string(all_entries[i].url_path, "' or ") || search_in_string(all_entries[i].url_path, "' and ") || search_in_string(all_entries[i].url_path, "1=1") || search_in_string(all_entries[i].url_path, "1' or '1'='1")) { annotate_entry(i, "SQL Injection"); suspicious_patterns_count++; } // 5. XSS ATTEMPTS: Cross-Site-Scripting Versuche if (search_in_string(all_entries[i].url_path, " 10) { annotate_entry(i, "Heavy Encoding"); suspicious_patterns_count++; } // 9. ATYPICAL HTTP METHODS: Ungewöhnliche oder fehlerhafte HTTP-Methoden if (strcmp(all_entries[i].request_method, "ATYPICAL") == 0) { annotate_entry(i, "Malformed Request"); suspicious_patterns_count++; } // Seltene aber potentiell gefährliche HTTP-Methoden else if (strcmp(all_entries[i].request_method, "PROPFIND") == 0 || strcmp(all_entries[i].request_method, "MKCOL") == 0 || strcmp(all_entries[i].request_method, "COPY") == 0 || strcmp(all_entries[i].request_method, "MOVE") == 0 || strcmp(all_entries[i].request_method, "LOCK") == 0) { annotate_entry(i, "Rare Method"); suspicious_patterns_count++; } // 10. STATUS CODE ANOMALIES: Verdächtige Status-Code Muster // 403 auf sensible Pfade könnte Angriffserkennung bedeuten if (all_entries[i].status_code == 403 && (search_in_string(all_entries[i].url_path, "admin") || search_in_string(all_entries[i].url_path, ".git") || search_in_string(all_entries[i].url_path, ".env"))) { annotate_entry(i, "Blocked Access"); suspicious_patterns_count++; } // 429 Too Many Requests - Rate Limiting aktiviert if (all_entries[i].status_code == 429) { annotate_entry(i, "Rate Limited"); suspicious_patterns_count++; } // 11. CREDENTIAL STUFFING: Wiederholte Login-Versuche mit verschiedenen Credentials if ((search_in_string(all_entries[i].url_path, "login") || search_in_string(all_entries[i].url_path, "signin") || search_in_string(all_entries[i].url_path, "auth")) && (all_entries[i].status_code == 401 || all_entries[i].status_code == 403)) { annotate_entry(i, "Failed Auth"); suspicious_patterns_count++; } // 12. SHELL/WEBSHELL ACCESS: Verdächtige Shell-bezogene Pfade if (search_in_string(all_entries[i].url_path, ".php?") || search_in_string(all_entries[i].url_path, "shell") || search_in_string(all_entries[i].url_path, "backdoor") || search_in_string(all_entries[i].url_path, "cmd=") || search_in_string(all_entries[i].url_path, "exec=") || search_in_string(all_entries[i].url_path, "system=")) { annotate_entry(i, "Shell Access"); suspicious_patterns_count++; } // 13. API ABUSE: Verdächtige API-Zugriffe if (search_in_string(all_entries[i].url_path, "/api/") && (all_entries[i].status_code >= 400 && all_entries[i].status_code < 500)) { annotate_entry(i, "API Error"); suspicious_patterns_count++; } } if (flag_verbose) printf("DEBUG: Analyse abgeschlossen. %d verdächtige Muster erkannt.\n", suspicious_patterns_count); } /* Parser. Regex Parser und strtok haben sich als schwieriger herausgestellt, da nginx Leerzeichen-getrennte Werte in den Logs hat, aber auch Leerzeichen innerhalb der Werte vorkommen. Daher ist das Parsing am einfachsten, wenn ein Pointer-basierter Algorithmus die Zeile Stück für Stück einliest und die Erwartungswerte in die struct schreibt. Fehleranfällig, wenn das Logformat nicht dem Standard entspricht - das gilt aber auch für andere Parser. */ // Standard-nginx-accesslog: /* log_format main '$remote_addr - $remote_user [$time_local] "$request" ' '$status $body_bytes_sent "$http_referer" ' '"$http_user_agent" "$http_x_forwarded_for"'; */ // 107.170.27.248 - - [31/Aug/2025:00:11:42 +0000] "GET /.git/config HTTP/1.1" 400 255 "-" "Mozilla/5.0; Keydrop.io/1.0(onlyscans.com/about);" "-" int parse_simple_log_line(char* line, int entry_index, char* source_file) { // Nimmt den Pointer auf die Zeile und einen Index entgegen - dieser ist anfangs 0 (globale Variable) und wird pro Eintrag inkrementiert all_entries[entry_index].annotated_flag = 0; char* current_pos = line; // leere Zeichen am Anfang überspringen current_pos = skip_spaces(current_pos); // kopieren der IP-Adresse in die globale all_entries Struktur unter gegebenen index // 107.170.27.248 - - [31/Aug/2025:00:11:42 +0000] "GET /.git/config HTTP/1.1" 400 255 "-" "Mozilla/5.0; Keydrop.io/1.0(onlyscans.com/about);" "-" // ^ copy_until_space(all_entries[entry_index].ip_address, current_pos, sizeof(all_entries[entry_index].ip_address)); // weiter zum nächsten Leerzeichen while (*current_pos != ' ' && *current_pos != '\0') current_pos++; // 107.170.27.248 - - [31/Aug/2025:00:11:42 +0000] "GET /.git/config HTTP/1.1" 400 255 "-" "Mozilla/5.0; Keydrop.io/1.0(onlyscans.com/about);" "-" // ^ // überspringe Leerzeichen current_pos = skip_spaces(current_pos); // 107.170.27.248 - - [31/Aug/2025:00:11:42 +0000] "GET /.git/config HTTP/1.1" 400 255 "-" "Mozilla/5.0; Keydrop.io/1.0(onlyscans.com/about);" "-" // ^ // mehrmaliges Überspringen des Wertes, der nicht von Interesse ist, Überspringen des Leerzeichens while (*current_pos != ' ' && *current_pos != '\0') current_pos++; current_pos = skip_spaces(current_pos); // 107.170.27.248 - - [31/Aug/2025:00:11:42 +0000] "GET /.git/config HTTP/1.1" 400 255 "-" "Mozilla/5.0; Keydrop.io/1.0(onlyscans.com/about);" "-" // ^ while (*current_pos != ' ' && *current_pos != '\0') current_pos++; current_pos = skip_spaces(current_pos); // 107.170.27.248 - - [31/Aug/2025:00:11:42 +0000] "GET /.git/config HTTP/1.1" 400 255 "-" "Mozilla/5.0; Keydrop.io/1.0(onlyscans.com/about);" "-" // ^ // Timestamp-Parsing if (*current_pos == '[') { current_pos++; // 107.170.27.248 - - [31/Aug/2025:00:11:42 +0000] "GET /.git/config HTTP/1.1" 400 255 "-" "Mozilla/5.0; Keydrop.io/1.0(onlyscans.com/about);" "-" // ^ // es folgt nach und nach das Einlesen von Datum und Uhrzeit, und wiederholtes Verschieben des Pointers all_entries[entry_index].time.day = 0; while (*current_pos >= '0' && *current_pos <= '9') {all_entries[entry_index].time.day = all_entries[entry_index].time.day * 10 + (*current_pos - '0'); current_pos++; } if (*current_pos == '/') current_pos++; char month_str[4] = {0}; int month_pos = 0; while (*current_pos != '/' && *current_pos != '\0' && month_pos < 3) {month_str[month_pos] = *current_pos; month_pos++; current_pos++; } all_entries[entry_index].time.month = month_name_to_number(month_str); if (*current_pos == '/') current_pos++; all_entries[entry_index].time.year = 0; while (*current_pos >= '0' && *current_pos <= '9') { all_entries[entry_index].time.year = all_entries[entry_index].time.year * 10 + (*current_pos - '0'); current_pos++; } if (*current_pos == ':') current_pos++; all_entries[entry_index].time.hour = 0; while (*current_pos >= '0' && *current_pos <= '9') { all_entries[entry_index].time.hour = all_entries[entry_index].time.hour * 10 + (*current_pos - '0'); current_pos++; } if (*current_pos == ':') current_pos++; all_entries[entry_index].time.minute = 0; while (*current_pos >= '0' && *current_pos <= '9') { all_entries[entry_index].time.minute = all_entries[entry_index].time.minute * 10 + (*current_pos - '0'); current_pos++; } if (*current_pos == ':') current_pos++; all_entries[entry_index].time.second = 0; while (*current_pos >= '0' && *current_pos <= '9') { all_entries[entry_index].time.second = all_entries[entry_index].time.second * 10 + (*current_pos - '0'); current_pos++; } // der Zeitzonen-Deskriptor wird übersprungen while (*current_pos != ']' && *current_pos != '\0') current_pos++; if (*current_pos == ']') current_pos++; // 107.170.27.248 - - [31/Aug/2025:00:11:42 +0000] "GET /.git/config HTTP/1.1" 400 255 "-" "Mozilla/5.0; Keydrop.io/1.0(onlyscans.com/about);" "-" // ^ } else { printf("ERROR: Unerwartetes Log-Format. Lediglich mit standard-nginx-accesslog kompatibel.\nDer Fehler ist beim Prüfen des Timestamps aufgetreten, dieser sollte folgendes Format haben:\n[DD/MMM/YYYY:HH:MM:SS +0000]\nLogeintrag: %s\n", line); cleanup_and_exit(); } current_pos = skip_spaces(current_pos); // Weiter mit dem String innerhalb "", aus dem die HTTP-Methode und der URL-Pfad zu entnehmen ist if (*current_pos == '"') { current_pos++; // 107.170.27.248 - - [31/Aug/2025:00:11:42 +0000] "GET /.git/config HTTP/1.1" 400 255 "-" "Mozilla/5.0; Keydrop.io/1.0(onlyscans.com/about);" "-" // ^ // lesen der HTTP-Methode in temporäre Variable char temp_method[50]; copy_until_space(temp_method, current_pos, sizeof(temp_method)); // Längenprüfung des Methodenstrings int is_valid_method = 1; if (strlen(temp_method) == 0 || strlen(temp_method) > 10) { is_valid_method = 0; } else { // prüfen, ob die Methode nur ASCII-Zeichen enthält for (int i = 0; temp_method[i] != '\0'; i++) { if (!((temp_method[i] >= 'A' && temp_method[i] <= 'Z') || (temp_method[i] >= 'a' && temp_method[i] <= 'z'))) { is_valid_method = 0; break; } } } if (is_valid_method) { // Normal parsen: HTTP-Methode bis zum nächsten Leerzeichen einlesen und speichern strncpy(all_entries[entry_index].request_method, temp_method, sizeof(all_entries[entry_index].request_method) - 1); all_entries[entry_index].request_method[sizeof(all_entries[entry_index].request_method) - 1] = '\0'; while (*current_pos != ' ' && *current_pos != '\0') { current_pos++; } current_pos = skip_spaces(current_pos); // 107.170.27.248 - - [31/Aug/2025:00:11:42 +0000] "GET /.git/config HTTP/1.1" 400 255 "-" "Mozilla/5.0; Keydrop.io/1.0(onlyscans.com/about);" "-" // ^ // Einlesen des URL-Path bis zum abschließenden " int i = 0; while (*current_pos != ' ' && *current_pos != '"' && *current_pos != '\0' && i < sizeof(all_entries[entry_index].url_path) - 1) { all_entries[entry_index].url_path[i] = *current_pos; i++; current_pos++; } all_entries[entry_index].url_path[i] = '\0'; while (*current_pos != '"' && *current_pos != '\0') current_pos++; if (*current_pos == '"') { current_pos++; } } else { // in NGINX treten gelegentlich fehlerhafte Requests auf, die binäre Daten übersenden, sodass normales parsen nicht möglich ist. // der entsprechende Eintrag wird daher mit dem String "ATYPICAL" repräsentiert strncpy(all_entries[entry_index].request_method, "ATYPICAL", sizeof(all_entries[entry_index].request_method) - 1); all_entries[entry_index].request_method[sizeof(all_entries[entry_index].request_method) - 1] = '\0'; // Read entire quoted content into url_path for forensic analysis int i = 0; while (*current_pos != '"' && *current_pos != '\0' && i < sizeof(all_entries[entry_index].url_path) - 1) { all_entries[entry_index].url_path[i] = *current_pos; i++; current_pos++; } all_entries[entry_index].url_path[i] = '\0'; // zum Ende des request-strings vorarbeiten, wenn der String zu lang war. while (*current_pos != '"' && *current_pos != '\0') { current_pos++; } if (*current_pos == '"') current_pos++; } // 107.170.27.248 - - [31/Aug/2025:00:11:42 +0000] "GET /.git/config HTTP/1.1" 400 255 "-" "Mozilla/5.0; Keydrop.io/1.0(onlyscans.com/about);" "-" // ^ } else { printf("ERROR: Unerwartetes Log-Format. Lediglich mit standard-nginx-accesslog kompatibel.\nDer Fehler ist beim Prüfen der HTTP-Methode aufgetreten. Diese steht innerhalb eines Strings zusammen mit dem URL-Pfad:\n\"GET /.git/config HTTP/1.1\"\nLogeintrag: %s\n", line); cleanup_and_exit(); } current_pos = skip_spaces(current_pos); // 107.170.27.248 - - [31/Aug/2025:00:11:42 +0000] "GET /.git/config HTTP/1.1" 400 255 "-" "Mozilla/5.0; Keydrop.io/1.0(onlyscans.com/about);" "-" // ^ // Parsen ded HTTP-Status Codes, der eine Zahl sein muss all_entries[entry_index].status_code = 0; while (*current_pos >= '0' && *current_pos <= '9') { all_entries[entry_index].status_code = all_entries[entry_index].status_code * 10 + (*current_pos - '0'); current_pos++; } current_pos = skip_spaces(current_pos); // genauso mit bytegröße der Anfrage // 107.170.27.248 - - [31/Aug/2025:00:11:42 +0000] "GET /.git/config HTTP/1.1" 400 255 "-" "Mozilla/5.0; Keydrop.io/1.0(onlyscans.com/about);" "-" // ^ all_entries[entry_index].bytes_sent = 0; while (*current_pos >= '0' && *current_pos <= '9') { all_entries[entry_index].bytes_sent = all_entries[entry_index].bytes_sent * 10 + (*current_pos - '0'); current_pos++; } current_pos = skip_spaces(current_pos); // Parsen des Referrer-Feldes innerhalb "" if (*current_pos == '"') { current_pos++; // öffnendes Anführungszeichen überspringen // Referrer-Inhalt zwischen "" einlesen int i = 0; while (*current_pos != '"' && *current_pos != '\0' && i < sizeof(all_entries[entry_index].referrer) - 1) { all_entries[entry_index].referrer[i] = *current_pos; i++; current_pos++; } all_entries[entry_index].referrer[i] = '\0'; if (*current_pos == '"') current_pos++; // schließendes Anführungszeichen überspringen } else { printf("ERROR: Unerwartetes Log-Format. Lediglich mit standard-nginx-accesslog kompatibel.\nDer Fehler ist beim Prüfen des Referrer-Feldes aufgetreten.\nLogeintrag: %s\n", line); cleanup_and_exit(); } current_pos = skip_spaces(current_pos); // parsen des user agents innerhalb "" if (*current_pos == '"') { current_pos++; int i = 0; while (*current_pos != '"' && *current_pos != '\0' && i < sizeof(all_entries[entry_index].user_agent) - 1) { all_entries[entry_index].user_agent[i] = *current_pos; i++; current_pos++; } all_entries[entry_index].user_agent[i] = '\0'; if (*current_pos == '"') current_pos++; } else { printf("ERROR: Unerwartetes Log-Format. Lediglich mit standard-nginx-accesslog kompatibel.\nDer Fehler ist beim Prüfen des User-Agent aufgetreten. Dieser steht innerhalb eines Strings:\n\"Mozilla/5.0; Keydrop.io/1.0(onlyscans.com/about);\"\nLogeintrag: %s\n", line); cleanup_and_exit(); } get_current_timestamp(all_entries[entry_index].parsing_timestamp, sizeof(all_entries[entry_index].parsing_timestamp)); // Dateinamen in das Feld schreiben - strncpy um Buffer overflow zu verhindern strncpy(all_entries[entry_index].source_file, source_file, sizeof(all_entries[entry_index].source_file) - 1); // strncpy setzt keinen Nullterminator, dieser muss am Ende eingefügt werden all_entries[entry_index].source_file[sizeof(all_entries[entry_index].source_file) - 1] = '\0'; return 1; } void load_regular_file(char* filename) { FILE* file = fopen(filename, "r"); if (file == NULL) { printf("ERROR: Kann Datei '%s' nicht öffnen!\n", filename); return; } printf("INFO: Lade Datei: %s\n", filename); char line[MAX_REQUEST_LENGTH]; int loaded_from_this_file = 0; while (fgets(line, sizeof(line), file) != NULL) { mem_expand_dynamically(); if (parse_simple_log_line(line, total_entries, filename)) { total_entries++; loaded_from_this_file++; } } fclose(file); printf(" -> %d Einträge aus dieser Datei geladen.\n", loaded_from_this_file); } // https://stackoverflow.com/questions/15417648/uncompressing-a-gz-string-in-c void load_gz_file(char* filename) { FILE *packedFileHandle = fopen(filename, "rb"); if (packedFileHandle == NULL) { printf("ERROR: Kann Datei '%s' nicht öffnen!\n", filename); return; } printf("INFO: Dekomprimiere Datei: %s\n", filename); // Temporäre Datei erstellen FILE *tempFile = tmpfile(); if (tempFile == NULL) { printf("ERROR: Kann temporäre Datei nicht erstellen!\n"); fclose(packedFileHandle); return; } // FileStream in temporäre Datei schreiben gzUnpackFile(packedFileHandle, tempFile); // Pointer an den Beginn der temporären Datei setzen rewind(tempFile); char line[MAX_REQUEST_LENGTH]; int loaded_from_this_file = 0; while (fgets(line, sizeof(line), tempFile) != NULL) { mem_expand_dynamically(); if (parse_simple_log_line(line, total_entries, filename)) { total_entries++; loaded_from_this_file++; } } fclose(tempFile); fclose(packedFileHandle); printf(" -> %d Einträge aus dieser Datei geladen.\n", loaded_from_this_file); } void load_log_file(char* path) { total_entries = 0; char full_path[512]; if (is_directory(path)) { if (flag_verbose) printf("DEBUG: Verzeichnis erkannt: %s\n", path); if (flag_verbose) printf("DEBUG: Suche nach .log Dateien...\n"); DIR* dir = opendir(path); if (dir == NULL) { printf("ERROR: Kann Verzeichnis '%s' nicht öffnen!\n", path); return; } struct dirent* entry; int files_found = 0; while ((entry = readdir(dir)) != NULL) { char* filename = (*entry).d_name; if (strcmp(filename, ".") == 0 || strcmp(filename, "..") == 0) { printf("WARNING: Überspringe Datei %s, unerwartete Dateisignatur\n", filename); continue; } if (strstr(filename, "error")!=NULL){ printf("WARNING: Überspringe Datei %s, vermutlich Errorlog\n", filename); printf("INFO: Error-Logs werden in dieser Version nicht unterstützt.\n"); continue; } int path_len = strlen(path); // Flag, um den Debug Print und den Export zu verbessern, ansonsten können // auftreten int needs_slash = (path_len > 0 && path[path_len - 1] != '/'); if (is_log_file(filename)) { (needs_slash) ? snprintf(full_path, sizeof(full_path), "%s/%s", path, filename) : snprintf(full_path, sizeof(full_path), "%s%s", path, filename); load_regular_file(full_path); files_found++; } else if (strstr(filename, ".gz") != NULL) { (needs_slash) ? snprintf(full_path, sizeof(full_path), "%s/%s", path, filename) : snprintf(full_path, sizeof(full_path), "%s%s", path, filename); load_gz_file(full_path); files_found++; } } closedir(dir); if (files_found == 0) { printf("WARNING: Keine .log Dateien im Verzeichnis gefunden.\n"); printf(" Tipp: Für .gz Dateien verwenden Sie 'gunzip *.gz' zum Dekomprimieren\n"); } else { printf("INFO: Insgesamt %d .log Dateien verarbeitet.\n", files_found); } } else { if (flag_verbose) printf("DEBUG: Einzelne Datei erkannt: %s\n", path); if (strstr(path, ".gz") != NULL) { load_gz_file(path); } else { load_regular_file(path); } } printf("INFO: Erfolgreich %d Einträge insgesamt geladen.\n", total_entries); // die aufgerufene Funktion ist KI-generiert und annotiert verdächtige Requests automatisch. annotate_suspicious_entries(all_entries); if (flag_verbose) printf("DEBUG: Aktueller Speicherverbrauch: %lu Bytes für %d Einträge\n", (unsigned long)(max_entries * sizeof(struct log_entry_t)), max_entries); } // Simplified filter functions using cleaner logic from shorter version // Filterfunktion für den User-Agent. Nimmt den Datensatz entgegen und prüft gegen die gesetzten Filter, gibt dann 0 oder 1 zurück int user_agent_matches(char* user_agent) { // kein Filter gesetzt - positiver Rückgabewert if (filters.user_agent_count == 0) return 1; // Ausschluss-Filter, jeden iterativ prüfen for (int i = 0; i < filters.user_agent_count; i++) { if (filters.user_agent_filters[i].filter_exclude_flag == 1) { if (search_in_string(user_agent, filters.user_agent_filters[i].pattern)) { return 0; // Ausschlussfilter aktiv, User Agent gefunden - nicht anzeigen } } } // Einschlussfilter int has_include_filters = 0; for (int i = 0; i < filters.user_agent_count; i++) { if (filters.user_agent_filters[i].filter_exclude_flag == 0) { has_include_filters = 1; if (search_in_string(user_agent, filters.user_agent_filters[i].pattern)) { return 1; // Ausschlussfilter inaktiv, Eintrag gefunden - anzeigen } } } // Einschlussfilter vorhanden, aber keine gefunden return !has_include_filters; } // Filterfunktion für URL-Pfad. Nimmt den Datensatz entgegen und prüft gegen die gesetzten Filter, gibt dann 0 oder 1 zurück int url_matches(char* url_path) { // kein Filter gesetzt - positiver Rückgabewert if (filters.url_count == 0) return 1; // Ausschluss-Filter, jeden iterativ prüfen for (int i = 0; i < filters.url_count; i++) { if (filters.url_filters[i].filter_exclude_flag == 1) { if (search_in_string(url_path, filters.url_filters[i].pattern)) { return 0; // Ausschlussfilter aktiv, URL-Pfad gefunden - nicht anzeigen } } } // Einschlussfilter int has_include_filters = 0; for (int i = 0; i < filters.url_count; i++) { if (filters.url_filters[i].filter_exclude_flag == 0) { has_include_filters = 1; if (search_in_string(url_path, filters.url_filters[i].pattern)) { return 1; // Ausschlussfilter inaktiv, Eintrag gefunden - anzeigen } } } // Einschlussfilter vorhanden, aber keine gefunden return !has_include_filters; } // Filterfunktion für Zugriffsmethode. Nimmt den Datensatz entgegen und prüft gegen die gesetzten Filter, gibt dann 0 oder 1 zurück int method_matches(char* request_method) { // kein Filter gesetzt - positiver Rückgabewert if (filters.method_count == 0) return 1; // Ausschluss-Filter, jeden iterativ prüfen for (int i = 0; i < filters.method_count; i++) { if (filters.method_filters[i].filter_exclude_flag == 1) { if (search_in_string(request_method, filters.method_filters[i].pattern)) { return 0; // Ausschlussfilter aktiv, HTTP-Methode gefunden - nicht anzeigen } } } // Einschlussfilter int has_include_filters = 0; for (int i = 0; i < filters.method_count; i++) { if (filters.method_filters[i].filter_exclude_flag == 0) { has_include_filters = 1; if (search_in_string(request_method, filters.method_filters[i].pattern)) { return 1; // Ausschlussfilter inaktiv, Eintrag gefunden - anzeigen } } } // Einschlussfilter vorhanden, aber keine gefunden return !has_include_filters; } // Filterfunktion für Status-Code. Nimmt den Datensatz entgegen und prüft gegen die gesetzten Filter, gibt dann 0 oder 1 zurück int status_code_matches(int status_code) { // kein Filter gesetzt - positiver Rückgabewert if (filters.status_count == 0) return 1; // Ausschluss-Filter, jeden iterativ prüfen for (int i = 0; i < filters.status_count; i++) { if (filters.status_filters[i].filter_exclude_flag == 1) { if (status_code == filters.status_filters[i].code) { return 0; // Ausschlussfilter aktiv, Status-Code gefunden - nicht anzeigen } } } // Einschlussfilter int has_include_filters = 0; for (int i = 0; i < filters.status_count; i++) { if (filters.status_filters[i].filter_exclude_flag == 0) { has_include_filters = 1; if (status_code == filters.status_filters[i].code) { return 1; // Ausschlussfilter inaktiv, Eintrag gefunden - anzeigen } } } // Einschlussfilter vorhanden, aber keine gefunden return !has_include_filters; } // Filterfunktion für IP-Adresse. Nimmt den Datensatz entgegen und prüft gegen die gesetzten Filter, gibt dann 0 oder 1 zurück int ip_address_matches(char* ip_address) { // kein Filter gesetzt - positiver Rückgabewert if (filters.ip_count == 0) return 1; // Ausschluss-Filter, jeden iterativ prüfen for (int i = 0; i < filters.ip_count; i++) { if (filters.ip_filters[i].filter_exclude_flag == 1) { if (search_in_string(ip_address, filters.ip_filters[i].ip_address)) { return 0; // Ausschlussfilter aktiv, IP-Adresse gefunden - nicht anzeigen } } } // Einschlussfilter int has_include_filters = 0; for (int i = 0; i < filters.ip_count; i++) { if (filters.ip_filters[i].filter_exclude_flag == 0) { has_include_filters = 1; if (search_in_string(ip_address, filters.ip_filters[i].ip_address)) { return 1; // Ausschlussfilter inaktiv, Eintrag gefunden - anzeigen } } } // Einschlussfilter vorhanden, aber keine gefunden return !has_include_filters; } int is_annotated(int annotated_flag){ if (filters.annotation_flag_filter.filter_exclude_flag == 1 && filters.annotation_flag_filter_enabled == 1 && annotated_flag == 1) { return 0; // zutreffender Ausschlussfilter führt zu negativem Rückgabewert } else if (filters.annotation_flag_filter.filter_exclude_flag == 0 && filters.annotation_flag_filter_enabled == 1 && annotated_flag == 1) { return 1; // zutreffender Einschlussfilter führt zu positivem Rückgabewert } // nichtannotiert, aber inklusiver Filter aktiv -> Eintrag nicht anzeigen else if (annotated_flag == 0 && filters.annotation_flag_filter.filter_exclude_flag == 0 && filters.annotation_flag_filter_enabled == 1){ return 0; } // Filter nicht aktiv, positiver Rückgabewert return 1; } int annotation_matches(char* annotation) { // kein Filter gesetzt - positiver Rückgabewert if (filters.annotation_count == 0) return 1; // Ausschluss-Filter, jeden iterativ prüfen for (int i = 0; i < filters.annotation_count; i++) { if (filters.annotation_filters[i].filter_exclude_flag == 1) { if (search_in_string(annotation, filters.annotation_filters[i].pattern)) { return 0; // Ausschlussfilter aktiv, Annotation gefunden - nicht anzeigen } } } // Einschlussfilter int has_include_filters = 0; for (int i = 0; i < filters.annotation_count; i++) { if (filters.annotation_filters[i].filter_exclude_flag == 0) { has_include_filters = 1; if (search_in_string(annotation, filters.annotation_filters[i].pattern)) { return 1; // Ausschlussfilter inaktiv, Eintrag gefunden - anzeigen } } } // Einschlussfilter vorhanden, aber keine gefunden return !has_include_filters; } // Vergleicht einen übergebenen Prüfwert für einen Zeitstempel mit dem aktuell gesetzten Filter. Wenn der Prüfwert im Filterbereich ist,wird 1 zurückgegeben. int time_matches(struct simple_time_t entry_time) { // kein Filter gesetzt - positiver Rückgabewert if (filters.time_count == 0) return 1; // Übergeordneter Ausschlussfilter for (int i = 0; i < filters.time_count; i++) { if (filters.time_filters[i].filter_exclude_flag == 1) { int in_range = (compare_times(entry_time, filters.time_filters[i].start_time) >= 0 && compare_times(entry_time, filters.time_filters[i].end_time) <= 0); if (in_range) { return 0; // zutreffender Ausschlussfilter führt zu negativem Rückgabewert, ist den Einschlussfiltern übergeordnet } } } // Einschlussfilter int has_include_filters = 0; for (int i = 0; i < filters.time_count; i++) { if (filters.time_filters[i].filter_exclude_flag == 0) { has_include_filters = 1; int in_range = (compare_times(entry_time, filters.time_filters[i].start_time) >= 0 && compare_times(entry_time, filters.time_filters[i].end_time) <= 0); if (in_range) { return 1; // Einschlussfilter passt } } } // Wenn Einschlussfilter vorhanden sind, aber keiner passt return !has_include_filters; } // Simplified passes_filter function using AND logic only int passes_filter(int entry_index) { // Simple AND logic - all filters must pass int status_match = status_code_matches(all_entries[entry_index].status_code); int method_match = method_matches(all_entries[entry_index].request_method); int ip_match = ip_address_matches(all_entries[entry_index].ip_address); int time_match = time_matches(all_entries[entry_index].time); int user_agent_match = user_agent_matches(all_entries[entry_index].user_agent); int url_match = url_matches(all_entries[entry_index].url_path); int has_annotation = is_annotated(all_entries[entry_index].annotated_flag); int annotation_match = annotation_matches(all_entries[entry_index].annotation); return status_match && ip_match && time_match && user_agent_match && method_match && url_match && has_annotation && annotation_match; } // Einfacher Zähler für alle Einträge, die die Filterfunktionen bestehen int count_filtered_entries(){ int count = 0; for (int i = 0; i < total_entries; i++) { if (passes_filter(i)) { count++; } } return count; } // notwendig, um konformes Timestamp-Format aus simple_time struct zu generieren. Unterstützt derzeit nur UTC void format_iso8601_time(struct simple_time_t time, char* buffer, int buffer_size) { snprintf(buffer, buffer_size, "%04d-%02d-%02dT%02d:%02d:%02d+00:00", time.year, time.month, time.day, time.hour, time.minute, time.second); } //Export in Timesketch-kompatiblem Format void write_csv_field(FILE* file, const char* field) { fprintf(file, "\""); for (const char* p = field; *p; p++) { if (*p == '"') { fprintf(file, "\"\""); } else { fprintf(file, "%c", *p); } } fprintf(file, "\""); } void export_filtered_entries(char *filepath) { // 90 chars +delimiter char filename[91]; if (filepath == NULL) { printf("Dateiname für Timesketch-Export eingeben (ohne .csv): "); if (scanf("%90s", filename) != 1) { printf("ERROR: Ungültiger Dateiname!\n"); clear_input_buffer(); return; } } else { strncpy(filename, filepath, sizeof(filename) - 1); filename[sizeof(filename) - 1] = '\0'; } strcat(filename, ".csv"); printf("\nINFO: Schreibe Datei %s...\n", filename); FILE* file = fopen(filename, "w"); if (file == NULL) { printf("ERROR: Kann Datei '%s' nicht erstellen!\n", filename); return; } // CSV-Kopfzeile für Timesketch-Kompatibilität (mit Referrer hinzugefügt) fprintf(file, "datetime,message,timestamp_desc,ip_address,method,url_path,status_code,bytes_sent,referrer,user_agent,parsing_timestamp,tag\n"); int exported_count = 0; char iso_datetime[32]; for (int i = 0; i < total_entries; i++) { if (passes_filter(i)) { format_iso8601_time(all_entries[i].time, iso_datetime, sizeof(iso_datetime)); // Tags werden von Timesketch in diesem Format erwartet: [ "tag1","tag2" ] char tag_str[256] = ""; if (strlen(all_entries[i].annotation) > 0) { // Create a working copy for strtok (it modifies the string) char annotation_copy[64]; strncpy(annotation_copy, all_entries[i].annotation, sizeof(annotation_copy) - 1); annotation_copy[sizeof(annotation_copy) - 1] = '\0'; strcat(tag_str, "[ "); // https://stackoverflow.com/questions/5901181/c-string-append // https://www.quora.com/How-can-we-count-the-number-of-special-characters-in-a-string-in-the-C-language-using-fgets char* token = strtok(annotation_copy, ","); int first = 1; while (token != NULL) { if (!first) { strcat(tag_str, ","); } strcat(tag_str, "\""); strncat(tag_str, token, sizeof(tag_str) - strlen(tag_str) - 3); strcat(tag_str, "\""); first = 0; token = strtok(NULL, ","); } strcat(tag_str, " ]"); } write_csv_field(file, iso_datetime); fprintf(file, ","); write_csv_field(file, all_entries[i].source_file); fprintf(file, ","); write_csv_field(file, "NGINX Audit Log"); fprintf(file, ","); write_csv_field(file, all_entries[i].ip_address); fprintf(file, ","); write_csv_field(file, all_entries[i].request_method); fprintf(file, ","); write_csv_field(file, all_entries[i].url_path); fprintf(file, ","); fprintf(file, "%d", all_entries[i].status_code); fprintf(file, ","); fprintf(file, "%d", all_entries[i].bytes_sent); fprintf(file, ","); write_csv_field(file, all_entries[i].referrer); fprintf(file, ","); write_csv_field(file, all_entries[i].user_agent); fprintf(file, ","); write_csv_field(file, all_entries[i].parsing_timestamp); fprintf(file, ","); if (strlen(all_entries[i].annotation) > 0) { write_csv_field(file, tag_str); } fprintf(file, "\n"); exported_count++; } } fclose(file); printf("INFO: %d Logeinträge erfolgreich als Timesketch-kompatible CSV-Datei nach '%s' exportiert.\n", exported_count, filename); } // zeigt alle annotierten Einträge detailliert an void show_annotated_entries() { printf("\nLOGEINTRÄGE MIT ANNOTATION\n"); printf("IP-Adresse | Methode | URL | Status | Bytes | User Agent | Zeit | Annotation\n"); printf("-----------------|---------|------------------------|--------|-------|--------------------------------------|------------------|--------------------\n"); int shown_count = 0; for (int i = 0; i < total_entries; i++) { if (!passes_filter(i)) continue; if (all_entries[i].annotated_flag == 1) { printf("%-16s | %-7s | %-22s | %-6d | %-5d | %-36s | %02d.%02d.%d %02d:%02d:%02d | %-18s\n", all_entries[i].ip_address, all_entries[i].request_method, all_entries[i].url_path, all_entries[i].status_code, all_entries[i].bytes_sent, all_entries[i].user_agent, all_entries[i].time.day, all_entries[i].time.month, all_entries[i].time.year, all_entries[i].time.hour, all_entries[i].time.minute, all_entries[i].time.second, all_entries[i].annotation ); shown_count++; } } if (shown_count == 0) { printf("Keine annotierten Einträge gefunden, die dem Filter entsprechen.\n"); } else { printf("\nInsgesamt %d annotierte Einträge angezeigt.\n", shown_count); } } // top-IP-Adressen sortieren nach Aufkommen und anzeigen void show_top_x_ips(){ // initialisieren von 1000 Datenstrukturen mit einem char ip_address[50] und int count (oben definiert) struct ip_stat_t ip_stats[1000]; // lokaler Zähler int unique_ips = 0; // iterieren über alle IP-Adressen, die im Filterset sind for (int i = 0; i < total_entries; i++) { if (!passes_filter(i)) continue; // IP lokal speichern char* current_ip = all_entries[i].ip_address; // initialisieren des Index - -1 heißt bisher nie aufgetreten int found_index = -1; for (int j = 0; j < unique_ips; j++) { // Wenn die IP-Adresse im Vergeichsdatensatz gefunden wird, wird der index entsprechend gesetzt/erstellt if (strcmp(ip_stats[j].ip_address, current_ip) == 0) { found_index = j; break; } } // wenn ein neuer index erstellt wurde (IP-Adresse war noch nicht in der Datenstruktur) oder gefunden wurde... if (found_index >= 0) { // inkrementieren des counters für Anzahl wie oft gefunden ip_stats[found_index].count++; } else { // wenn nicht gefunden (found_index == -1) wird die IP mit count = 1 neu angelegt if (unique_ips < 1000) { strncpy(ip_stats[unique_ips].ip_address, current_ip, sizeof(ip_stats[unique_ips].ip_address) - 1); ip_stats[unique_ips].ip_address[sizeof(ip_stats[unique_ips].ip_address) - 1] = '\0'; ip_stats[unique_ips].count = 1; unique_ips++; } } } // iterieren über alle unique ips // Bubble sort: https://www.proggen.org/doku.php?id=training:sorting:bubblesort:solution for (int i = 0; i < unique_ips - 1; i++) { for (int j = 0; j < unique_ips - i - 1; j++) { if (ip_stats[j].count < ip_stats[j + 1].count) { struct ip_stat_t temp = ip_stats[j]; ip_stats[j] = ip_stats[j + 1]; ip_stats[j + 1] = temp; } } } // visuelle Darstellung printf("\nTOP %d IP-ADRESSEN\n", TOP_X); printf("Rang | IP-Adresse | Anzahl Anfragen\n"); printf("-----|------------------|----------------\n"); int show_count = (unique_ips < TOP_X) ? unique_ips : TOP_X; for (int i = 0; i < show_count; i++) { printf("%-4d | %-16s | %d\n", i + 1, ip_stats[i].ip_address, ip_stats[i].count); } if (unique_ips == 0) { printf("Keine IP-Adressen in den gefilterten Daten gefunden.\n"); } else { printf("\nInsgesamt %d verschiedene IP-Adressen gefunden.\n", unique_ips); } } // gleiche Mechanik wie in show_top_x_ips - könnte wahrscheinlich vereinheitlicht werden void show_top_user_agents(){ struct user_agent_stat_t { char user_agent[256]; int count; }; struct user_agent_stat_t agent_stats[1000]; int unique_agents = 0; for (int i = 0; i < total_entries; i++) { if (!passes_filter(i)) continue; char* current_agent = all_entries[i].user_agent; int found_index = -1; for (int j = 0; j < unique_agents; j++) { if (strcmp(agent_stats[j].user_agent, current_agent) == 0) { found_index = j; break; } } if (found_index >= 0) { agent_stats[found_index].count++; } else { if (unique_agents < 1000) { strncpy(agent_stats[unique_agents].user_agent, current_agent, sizeof(agent_stats[unique_agents].user_agent) - 1); agent_stats[unique_agents].user_agent[sizeof(agent_stats[unique_agents].user_agent) - 1] = '\0'; agent_stats[unique_agents].count = 1; unique_agents++; } } } for (int i = 0; i < unique_agents - 1; i++) { for (int j = 0; j < unique_agents - i - 1; j++) { if (agent_stats[j].count < agent_stats[j + 1].count) { struct user_agent_stat_t temp = agent_stats[j]; agent_stats[j] = agent_stats[j + 1]; agent_stats[j + 1] = temp; } } } printf("\nTOP %d USER AGENTS\n", TOP_X); printf("Rang | User Agent | Anzahl Anfragen\n"); printf("-----|--------------------------------------|----------------\n"); int show_count = (unique_agents < TOP_X) ? unique_agents : TOP_X; for (int i = 0; i < show_count; i++) { printf("%-4d | %-34s | %d\n", i + 1, agent_stats[i].user_agent, agent_stats[i].count); } if (unique_agents == 0) { printf("Keine User Agents in den gefilterten Daten gefunden.\n"); } else { printf("\nInsgesamt %d verschiedene User Agents gefunden.\n", unique_agents); } } // Zeigt jeden Eintrag, der den Filter besteht. Wird ein int !=0 übergeben, wird nur die spezifizierte Anzahl an Einträgen dargestellt. // Verwendung in einer Komplettausgabe sowie im Preview void show_filtered_entries(int num_shown) { int shown_count = 0; printf("\nLOGDATEN:\n"); printf("IP-Adresse | Methode | URL | Status | Bytes | User Agent | Zeit | Annotation\n"); printf("-----------------|---------|------------------------|--------|-------|--------------------------------------|---------------------|--------------------\n"); for (int i = 0; i < total_entries; i++) { if (!passes_filter(i)) continue; printf("%-16s | %-7s | %-22s | %-6d | %-5d | %-36s | %02d.%02d.%d %02d:%02d:%02d | %31s\n", all_entries[i].ip_address, all_entries[i].request_method, all_entries[i].url_path, all_entries[i].status_code, all_entries[i].bytes_sent, all_entries[i].user_agent, all_entries[i].time.day, all_entries[i].time.month, all_entries[i].time.year, all_entries[i].time.hour, all_entries[i].time.minute, all_entries[i].time.second, all_entries[i].annotation ); shown_count++; if (num_shown != 0 && shown_count >= num_shown) break; // für Preview: abbrechen wenn num_shown erreicht } if (num_shown != 0) { printf("\nInsgesamt %d Einträge in der Vorschau.\n", shown_count); } else { printf("\nInsgesamt %d Einträge gefunden.\n", shown_count); } if (shown_count == 0) { printf("Keine Einträge gefunden, die dem Filter entsprechen.\n"); } } // Diese Funktion zeigt stets die gesetzten Filter in einem Format an, das aus dem interaktiven Modus reproduzierbar im cli-Modus wiederverwendet werden kann void print_filter_args(){ int total_filters = filters.status_count + filters.method_count + filters.ip_count + filters.time_count + filters.user_agent_count + filters.url_count + filters.annotation_flag_filter_enabled + filters.annotation_count; if (total_filters == 0) { return; } printf("-f "); if (filters.status_count > 0) { printf("--status="); for (int i = 0; i < filters.status_count; i++) { if (i > 0) {printf(",");}; if (filters.status_filters[i].filter_exclude_flag == 1) printf("!"); printf("%d", filters.status_filters[i].code); } printf(" "); } if (filters.method_count > 0) { printf("--method="); for (int i = 0; i < filters.method_count; i++) { if (i > 0) {printf(",");} if (filters.method_filters[i].filter_exclude_flag == 1) printf("!"); printf("%s", filters.method_filters[i].pattern); } printf(" "); } if (filters.ip_count > 0) { printf("--ip="); for (int i = 0; i < filters.ip_count; i++) { if (i > 0) {printf(",");}; if (filters.ip_filters[i].filter_exclude_flag == 1) printf("!"); printf("%s", filters.ip_filters[i].ip_address); } printf(" "); } if (filters.user_agent_count > 0) { printf("--useragent="); for (int i = 0; i < filters.user_agent_count; i++) { if (i > 0) {printf(",");}; if (filters.user_agent_filters[i].filter_exclude_flag == 1) printf("!"); printf("%s", filters.user_agent_filters[i].pattern); } printf(" "); } if (filters.url_count > 0) { printf("--url="); for (int i = 0; i < filters.url_count; i++) { if (i > 0) {printf(",");}; if (filters.url_filters[i].filter_exclude_flag == 1) printf("!"); printf("%s", filters.url_filters[i].pattern); } printf(" "); } if (filters.time_count >0){ printf("--timerange="); for(int i =0; i < filters.time_count;i++){ if(i > 0) {printf(",");}; if (filters.time_filters[i].filter_exclude_flag == 1) {printf("!");}; // die führenden 0 sind hier wichtig printf("%04d-%02d-%02d-%02d-%02d-%02d:%04d-%02d-%02d-%02d-%02d-%02d ", filters.time_filters[i].start_time.year, filters.time_filters[i].start_time.month, filters.time_filters[i].start_time.day, filters.time_filters[i].start_time.hour, filters.time_filters[i].start_time.minute, filters.time_filters[i].start_time.second, filters.time_filters[i].end_time.year, filters.time_filters[i].end_time.month, filters.time_filters[i].end_time.day, filters.time_filters[i].end_time.hour, filters.time_filters[i].end_time.minute, filters.time_filters[i].end_time.second); } } // macht nicht viel Sinn, diese hier zu integrieren, so lang der User nicht weiß, wie die Annotationen lauten if (filters.annotation_flag_filter_enabled) { printf("--annotated="); if (filters.annotation_flag_filter.filter_exclude_flag == 1) printf("!"); printf("true "); } if (filters.annotation_count > 0) { printf("--annotation="); for (int i = 0; i < filters.annotation_count; i++) { if (i > 0) printf(","); if (filters.annotation_filters[i].filter_exclude_flag == 1) printf("!"); printf("%s", filters.annotation_filters[i].pattern); } printf(" "); } printf("\n"); } // Status-Anzeige, die in jedem Bildschirm dynamisch eine Vorschau des gefilterten Datensatzes, sowie eine Übersicht über die gesetzten Filter anzeigt. void show_status(){ printf("\nPREVIEW:\n"); // diese Funktion wird auch an anderer Stelle mit Parameter 0 verwendet und zeigt dann alles an show_filtered_entries(10); printf("\nSTATUS\n"); if (total_entries > 0) { printf(" %d Logzeilen in Datenstruktur\n", total_entries); printf(" Speicherbelegung: %lu Bytes\n", (unsigned long)(max_entries * sizeof(struct log_entry_t))); } else { printf(" ERROR: Keine Einträge in Datenstruktur!\n"); } printf("\n Aktive Filter:\n"); int total_filters = filters.status_count + filters.method_count + filters.ip_count + filters.time_count + filters.user_agent_count + filters.url_count + filters.annotation_flag_filter_enabled + filters.annotation_count; if (total_filters == 0) { printf(" -> keine Filter gesetzt\n"); } else { printf("\n "); print_filter_args(); printf("\n \n"); printf(" Ausschlussfilter (!) haben Vorrang, dann Einschlussfilter\n"); printf(" Filter-Logik: UND-Verknüpfung zwischen Kategorien\n"); } if (total_entries > 0) { int filtered_count = count_filtered_entries(); printf("\n DATENSATZ: \n %d von %d Einträgen entsprechen den Filtern\n", filtered_count, total_entries); printf(" Außergewöhnliche Muster gefunden: %d\n", suspicious_patterns_count); } } // TRANSPARENZ: Dies wurde KI-generiert void print_filter_examples(){ printf("\nFILTER-DOKUMENTATION\n"); printf("===================\n\n"); printf("GRUNDLEGENDE SYNTAX:\n"); printf(" --filtertyp=wert1,wert2,wert3 Mehrere Werte kommagetrennt\n"); printf(" --filtertyp=!wert Ausschluss (mit ! Präfix)\n"); printf(" --filtertyp=wert1,!wert2 Gemischt: wert1 einschließen, wert2 ausschließen\n\n"); printf("VERFÜGBARE FILTER:\n"); printf(" --status=200,404,500 HTTP-Status-Codes\n"); printf(" --ip=192.168.1.100,10.0.0.5 IP-Adressen (exakt)\n"); printf(" --method=GET,POST,ATYPICAL HTTP-Methoden\n"); printf(" --useragent=bot,crawler,scanner User-Agent Teilstrings\n"); printf(" --url=.git,.env,wp-admin URL-Pfad Teilstrings\n"); printf(" --annotated=true Nur annotierte Einträge\n"); printf(" --annotation=SQL,XSS Annotations-Teilstrings\n"); printf(" --timerange=START:END Zeiträume (siehe unten)\n\n"); printf("ZEITRAUM-SYNTAX:\n"); printf(" Format: YYYY-MM-DD-HH-MM-SS:YYYY-MM-DD-HH-MM-SS\n"); printf(" Beispiel: 2025-08-31-08-00-00:2025-08-31-18-00-00\n"); printf(" Ausschluss: !2025-08-31-02-00-00:2025-08-31-06-00-00\n\n"); printf("FILTER-LOGIK:\n"); printf(" 1. Ausschlussfilter (!) haben IMMER Vorrang\n"); printf(" 2. UND-Verknüpfung zwischen verschiedenen Filtertypen\n"); printf(" 3. ODER-Verknüpfung innerhalb gleicher Filtertypen\n\n"); printf("PRAKTISCHE BEISPIELE:\n\n"); printf("SICHERHEITSANALYSE:\n"); printf("Verdächtige Pfad-Zugriffe finden:\n"); printf(" --url=.git,.env,wp-admin,phpmyadmin\n"); printf(" > Zeigt Zugriffe auf Git-Repos, Config-Dateien oder Admin-Bereiche\n\n"); printf("SQL-Injection Versuche:\n"); printf(" --annotation=SQL --url=select,union,drop\n"); printf(" > Kombiniert automatische Erkennung mit manuellen Mustern\n\n"); printf("Bot-Traffic ausschließen:\n"); printf(" --useragent=!bot,!crawler,!spider --status=200\n"); printf(" > Nur erfolgreiche Requests ohne Bot-Traffic\n\n"); printf("PERFORMANCE-ANALYSE:\n"); printf("Server-Fehler in Geschäftszeiten:\n"); printf(" --timerange=2025-08-31-08-00-00:2025-08-31-18-00-00 --status=500,502,503\n"); printf(" > 5xx Fehler nur während Arbeitszeit\n\n"); printf("Wartungszeit ausschließen:\n"); printf(" --timerange=!2025-08-31-02-00-00:2025-08-31-06-00-00 --status=!200\n"); printf(" > Alle Fehler außerhalb der Wartungszeit\n\n"); printf("ANOMALIE-ERKENNUNG:\n"); printf("Alle verdächtigen Aktivitäten:\n"); printf(" --annotated=true\n"); printf(" > Zeigt alle automatisch erkannten Anomalien\n\n"); printf("Lange Payloads von bestimmten IPs:\n"); printf(" --annotation=Long --ip=!192.168.1.0\n"); printf(" > Verdächtig lange Requests von externen IPs\n\n"); printf("FORENSIK:\n"); printf("Angriffsmuster in kritischem Zeitfenster:\n"); printf(" --timerange=2025-08-31-14-30-00:2025-08-31-15-30-00 --status=403,404 --annotation=Scanner,SQL,XSS\n"); printf(" > Detaillierte Analyse eines Sicherheitsvorfalls\n\n"); printf("Fehlgeschlagene Login-Versuche:\n"); printf(" --url=login,signin,auth --status=401,403 --annotation=Failed\n"); printf(" > Brute-Force Angriffe auf Login-Bereiche\n\n"); printf("TRAFFIC-BEREINIGUNG:\n"); printf("Nur menschlicher Traffic:\n"); printf(" --useragent=!bot,!crawler,!scanner --method=!PROPFIND --status=200,304\n"); printf(" > Erfolgreiche Requests ohne automatisierten Traffic\n\n"); printf("Administrative Zugriffe ausschließen:\n"); printf(" --ip=!192.168.1.10,!10.0.0.100 --url=!admin,!manager\n"); printf(" > Traffic ohne Admin-IPs und Admin-Bereiche\n\n"); printf("HÄUFIGE KOMBINATIONEN:\n"); printf("DDoS-Verdacht:\n"); printf(" --status=429,503,502 --annotation=Rate\n"); printf(" > Überlastungsindikationen und Rate-Limiting\n\n"); printf("Webshell-Suche:\n"); printf(" --url=.php,cmd=,exec= --annotation=Shell --method=POST,GET\n"); printf(" > Verdächtige PHP-Aufrufe und Shell-Aktivitäten\n\n"); printf("Normale Website-Nutzung:\n"); printf(" --status=200,304 --method=GET,POST --useragent=!bot --annotated=!true\n"); printf(" > Sauberer, erfolgreicher Website-Traffic ohne Anomalien\n\n"); printf("ANNOTATIONEN (automatisch erkannt):\n"); printf(" Long Payload - Verdächtig lange Anfragen\n"); printf(" Git Access - Zugriff auf .git Verzeichnisse\n"); printf(" Config Access - Zugriff auf Konfigurationsdateien\n"); printf(" WP Attack - WordPress-spezifische Angriffe\n"); printf(" SQL Injection - SQL-Injection Versuche\n"); printf(" XSS Attempt - Cross-Site-Scripting Versuche\n"); printf(" Scanner/Bot - Automatisierte Scanner\n"); printf(" Path Traversal - Directory-Traversal Angriffe\n"); printf(" Failed Auth - Fehlgeschlagene Authentifizierung\n"); printf(" Shell Access - Webshell-Zugriffe\n"); } int handle_menu_shortcuts(int choice) { if (choice == -2) { return -2; } else if (choice == -3) { return -3; } else if (choice == -4) { printf("Programmende\n"); cleanup_memory(); exit(0); } return choice; } // überall wo integer aus String-Input gelesen werden müssen. basiert auf strtol, was gegen Buffer Overflow sicher sein soll int safe_read_integer(char* prompt, int min_val, int max_val) { char input[50]; int value; char *endptr; // endlos while (1) { printf("%s", prompt); // scanf liest den Input in einen pointer ein, daher nicht &input. Die Usereingabe ist ein String, also ein Array if (scanf("%49s", input) != 1) { clear_input_buffer(); printf("ERROR: Ungültige Eingabe. Bitte erneut versuchen.\n"); continue; } clear_input_buffer(); // Standard Rückgabewerte für die Menünavigation, die Werte werden für die choice-Variable genutzt if (strcmp(input, "b") == 0 || strcmp(input, "B") == 0) return -2; if (strcmp(input, "m") == 0 || strcmp(input, "M") == 0) return -3; if (strcmp(input, "q") == 0 || strcmp(input, "Q") == 0) return -4; // Konvertierung der Eingabe in einen Long-Integer der Basis 10. Der endptr speichert einen Pointer auf das erste ungültige Zeichen nach einlesen des Lon-Integer value = strtol(input, &endptr, 10); // wenn der endptr der Nullterminator ist, handelte es sich bei der Eingabe sicher um einen Long-Integer. if (*endptr != '\0') { printf("ERROR: '%s' ist keine gültige Zahl. Bitte erneut versuchen.\n", input); continue; } // Prüfen, ob sich der Wert im Erwartungsbereich befindet if (value < min_val || value > max_val) { printf("ERROR: Wert muss zwischen %d und %d liegen. Bitte erneut versuchen.\n", min_val, max_val); continue; } return value; } } int safe_read_string(char* prompt, char* buffer, int buffer_size) { while (1) { printf("%s", prompt); if (scanf("%s", buffer) != 1) { clear_input_buffer(); printf("ERROR: Ungültige Eingabe. Bitte erneut versuchen.\n"); continue; } clear_input_buffer(); if (strlen(buffer) >= buffer_size - 1) { printf("ERROR: Eingabe zu lang. Bitte erneut versuchen.\n"); continue; } if (strcmp(buffer, "b") == 0 || strcmp(buffer, "B") == 0) return -2; if (strcmp(buffer, "m") == 0 || strcmp(buffer, "M") == 0) return -3; if (strcmp(buffer, "q") == 0 || strcmp(buffer, "Q") == 0) return -4; return 0; } } // Menü mit standardisierter Navigation int read_menu_input(){ char input[10]; // scanf braucht eine Begrenzung, %s würde alle Zeichen in stdin lesen - buffer overflow if (scanf("%5s", input) != 1) { clear_input_buffer(); return -1; } clear_input_buffer(); if (strcmp(input, "b") == 0 || strcmp(input, "B") == 0) return -2; if (strcmp(input, "m") == 0 || strcmp(input, "M") == 0) return -3; if (strcmp(input, "q") == 0 || strcmp(input, "Q") == 0) return -4; char *endptr; //string to long long number = strtol(input, &endptr, 10); // Prüfen, ob Eingabe erfolgreich zu Zahl umgewandelt werden konnte - endptr speichert das Zeichen, das nicht mehr gepasst hat - muss also Nullterminator sein if (*endptr != '\0' || number < 0 || number > 999) { return -1; } return (int)number; } // Transparenz: Die visuelle Darstellung der Menus und dessen Formatierung, sowie repetitive schriftliche Wiedergabe wie etwa die interaktive Eingabe der Zeiträume ist teilweise KI-generiert. void show_main_menu(){ printf("\nHAUPTMENÜ\n"); printf("1. Filter verwalten\n"); printf("2. Daten anzeigen und exportieren\n"); printf("3. Export (CSV, Timesketch-kompatibel)\n"); printf("4. Programm beenden\n"); printf("Navigation: [b]Zurück [m]Hauptmenü [q]Beenden\n"); printf("Auswahl: "); } int menu_set_filters(){ int choice = 0; // Standardnavigation aus read_menu_input while (choice != -2 && choice != -3) { show_status(); printf("\nFILTER HINZUFÜGEN\n"); printf("1. Status-Code (exakte Suche)\n"); printf("2. IP-Adresse (exakte Suche)\n"); printf("3. Zeitraum (Start- und Endzeit)\n"); printf("4. User-Agent (Teilstring-Suche)\n"); printf("5. HTTP-Methode (Teilstring-Suche)\n"); printf("6. URL-Pfad (Teilstring-Suche)\n"); printf("7. Hat Annotation\n"); printf("8. Annotation (Teilstring-Suche)\n"); printf("Navigation: [b]Zurück [m]Hauptmenü [q]Beenden\n"); printf("Auswahl: "); choice = read_menu_input(); choice = handle_menu_shortcuts(choice); if (choice == 1) { if (filters.status_count >= MAX_FILTERS) { printf("ERROR: Maximale Anzahl Status-Code Filter erreicht (%d)!\n", MAX_FILTERS); continue; } int status = safe_read_integer("Status Code eingeben (z.B. 200, 404, 500): ", 100, 599); if (status < 0) continue; printf("\nFilter-Typ wählen:\n"); printf("1. Einschließen (nur Status %d anzeigen)\n", status); printf("2. Ausschließen (Status %d NICHT anzeigen)\n", status); int filter_type = safe_read_integer("Auswahl: ", 1, 2); if (filter_type < 0) continue; filters.status_filters[filters.status_count].code = status; filters.status_filters[filters.status_count].filter_exclude_flag = (filter_type == 2) ? 1 : 0; filters.status_count++; printf("Status-Code Filter hinzugefügt. Gesamt: %d\n", filters.status_count); } else if (choice == 2) { if (filters.ip_count >= MAX_FILTERS) { printf("ERROR: Maximale Anzahl IP-Filter erreicht (%d)!\n", MAX_FILTERS); continue; } char ip[50]; int result = safe_read_string("IP-Adresse eingeben: ", ip, sizeof(ip)); if (result < 0) continue; printf("\nFilter-Typ wählen:\n"); printf("1. Einschließen (nur IP %s anzeigen)\n", ip); printf("2. Ausschließen (IP %s NICHT anzeigen)\n", ip); int filter_type = safe_read_integer("Auswahl: ", 1, 2); if (filter_type < 0) continue; strncpy(filters.ip_filters[filters.ip_count].ip_address, ip, sizeof(filters.ip_filters[filters.ip_count].ip_address) - 1); filters.ip_filters[filters.ip_count].ip_address[sizeof(filters.ip_filters[filters.ip_count].ip_address) - 1] = '\0'; filters.ip_filters[filters.ip_count].filter_exclude_flag = (filter_type == 2) ? 1 : 0; filters.ip_count++; printf("IP-Filter hinzugefügt. Gesamt: %d\n", filters.ip_count); } else if (choice == 3) { if (filters.time_count >= MAX_FILTERS) { printf("ERROR: Maximale Anzahl Zeitraum-Filter erreicht (%d)!\n", MAX_FILTERS); continue; } printf("\nZEITRAUM FILTER HINZUFÜGEN\n"); struct time_filter_t new_time_filter = {0}; printf("STARTZEIT:\n"); int start_year = safe_read_integer("Jahr (z.B. 2025): ", 1970, 2100); if (start_year < 0) continue; int start_month = safe_read_integer("Monat (1-12): ", 1, 12); if (start_month < 0) continue; int start_day = safe_read_integer("Tag (1-31): ", 1, 31); if (start_day < 0) continue; int start_hour = safe_read_integer("Stunde (0-23): ", 0, 23); if (start_hour < 0) continue; int start_minute = safe_read_integer("Minute (0-59): ", 0, 59); if (start_minute < 0) continue; int start_second = safe_read_integer("Sekunde (0-59): ", 0, 59); if (start_second < 0) continue; printf("\nENDZEIT:\n"); int end_year = safe_read_integer("Jahr (z.B. 2025): ", 1970, 2100); if (end_year < 0) continue; int end_month = safe_read_integer("Monat (1-12): ", 1, 12); if (end_month < 0) continue; int end_day = safe_read_integer("Tag (1-31): ", 1, 31); if (end_day < 0) continue; int end_hour = safe_read_integer("Stunde (0-23): ", 0, 23); if (end_hour < 0) continue; int end_minute = safe_read_integer("Minute (0-59): ", 0, 59); if (end_minute < 0) continue; int end_second = safe_read_integer("Sekunde (0-59): ", 0, 59); if (end_second < 0) continue; printf("\nFilter-Typ wählen:\n"); printf("1. Einschließen (nur Ereignisse IN diesem Zeitraum)\n"); printf("2. Ausschließen (Ereignisse in diesem Zeitraum NICHT anzeigen)\n"); int filter_type = safe_read_integer("Auswahl: ", 1, 2); if (filter_type < 0) continue; new_time_filter.start_time.year = start_year; new_time_filter.start_time.month = start_month; new_time_filter.start_time.day = start_day; new_time_filter.start_time.hour = start_hour; new_time_filter.start_time.minute = start_minute; new_time_filter.start_time.second = start_second; new_time_filter.end_time.year = end_year; new_time_filter.end_time.month = end_month; new_time_filter.end_time.day = end_day; new_time_filter.end_time.hour = end_hour; new_time_filter.end_time.minute = end_minute; new_time_filter.end_time.second = end_second; new_time_filter.filter_exclude_flag = (filter_type == 2) ? 1 : 0; filters.time_filters[filters.time_count] = new_time_filter; filters.time_count++; printf("Zeitraum-Filter hinzugefügt. Gesamt: %d\n", filters.time_count); } else if (choice == 4) { if (filters.user_agent_count >= MAX_FILTERS) { printf("ERROR: Maximale Anzahl User-Agent Filter erreicht (%d)!\n", MAX_FILTERS); continue; } char pattern[256]; int result = safe_read_string("User-Agent Suchtext eingeben (z.B. 'chrome', 'bot', 'scanner'): ", pattern, sizeof(pattern)); if (result < 0) continue; printf("\nFilter-Typ wählen:\n"); printf("1. Einschließen (nur User-Agents mit '%s')\n", pattern); printf("2. Ausschließen (User-Agents mit '%s' NICHT anzeigen)\n", pattern); int filter_type = safe_read_integer("Auswahl: ", 1, 2); if (filter_type < 0) continue; strncpy(filters.user_agent_filters[filters.user_agent_count].pattern, pattern, sizeof(filters.user_agent_filters[filters.user_agent_count].pattern) - 1); filters.user_agent_filters[filters.user_agent_count].pattern[sizeof(filters.user_agent_filters[filters.user_agent_count].pattern) - 1] = '\0'; filters.user_agent_filters[filters.user_agent_count].filter_exclude_flag = (filter_type == 2) ? 1 : 0; filters.user_agent_count++; printf("User-Agent Filter hinzugefügt. Gesamt: %d\n", filters.user_agent_count); } else if (choice == 5) { if (filters.method_count >= MAX_FILTERS) { printf("ERROR: Maximale Anzahl Method-Filter erreicht (%d)!\n", MAX_FILTERS); continue; } char pattern[10]; int result = safe_read_string("HTTP-Methode eingeben (z.B. 'GET', 'POST', 'PUT', Sonderwert: 'ATYPICAL'): ", pattern, sizeof(pattern)); if (result < 0) continue; printf("\nFilter-Typ wählen:\n"); printf("1. Einschließen (nur Methode '%s')\n", pattern); printf("2. Ausschließen (Methode '%s' NICHT anzeigen)\n", pattern); int filter_type = safe_read_integer("Auswahl: ", 1, 2); if (filter_type < 0) continue; strncpy(filters.method_filters[filters.method_count].pattern, pattern, sizeof(filters.method_filters[filters.method_count].pattern) - 1); filters.method_filters[filters.method_count].pattern[sizeof(filters.method_filters[filters.method_count].pattern) - 1] = '\0'; filters.method_filters[filters.method_count].filter_exclude_flag = (filter_type == 2) ? 1 : 0; filters.method_count++; printf("Method-Filter hinzugefügt. Gesamt: %d\n", filters.method_count); } else if (choice == 6) { if (filters.url_count >= MAX_FILTERS) { printf("ERROR: Maximale Anzahl URL-Filter erreicht (%d)!\n", MAX_FILTERS); continue; } char pattern[MAX_REQUEST_LENGTH]; int result = safe_read_string("URL-Pfad Suchtext eingeben (z.B. '.git', '.php', '/admin', 'wp-'): ", pattern, sizeof(pattern)); if (result < 0) continue; printf("\nFilter-Typ wählen:\n"); printf("1. Einschließen (nur URLs mit '%s')\n", pattern); printf("2. Ausschließen (URLs mit '%s' NICHT anzeigen)\n", pattern); int filter_type = safe_read_integer("Auswahl: ", 1, 2); if (filter_type < 0) continue; strncpy(filters.url_filters[filters.url_count].pattern, pattern, sizeof(filters.url_filters[filters.url_count].pattern) - 1); filters.url_filters[filters.url_count].pattern[sizeof(filters.url_filters[filters.url_count].pattern) - 1] = '\0'; filters.url_filters[filters.url_count].filter_exclude_flag = (filter_type == 2) ? 1 : 0; filters.url_count++; printf("URL-Filter hinzugefügt. Gesamt: %d\n", filters.url_count); } else if (choice == 7) { printf("\nFilter-Typ wählen:\n"); printf("1. Einschließen (nur Einträge mit Annotationen)\n"); printf("2. Ausschließen (Einträge mit Annotationen NICHT anzeigen)\n"); int filter_type = safe_read_integer("Auswahl: ", 1, 2); if (filter_type < 0) continue; filters.annotation_flag_filter.filter_exclude_flag = (filter_type == 2) ? 1 : 0; filters.annotation_flag_filter_enabled = 1; printf("Annotations-Flag Filter hinzugefügt.\n"); } else if (choice == 8) { if (filters.annotation_count >= MAX_FILTERS) { printf("ERROR: Maximale Anzahl Annotations-Filter erreicht (%d)!\n", MAX_FILTERS); continue; } char pattern[64]; int result = safe_read_string("Annotations-Suchtext eingeben (z.B. 'Long Payload', 'SQL'): ", pattern, sizeof(pattern)); if (result < 0) continue; printf("\nFilter-Typ wählen:\n"); printf("1. Einschließen (nur Annotationen mit '%s')\n", pattern); printf("2. Ausschließen (Annotationen mit '%s' NICHT anzeigen)\n", pattern); int filter_type = safe_read_integer("Auswahl: ", 1, 2); if (filter_type < 0) continue; strncpy(filters.annotation_filters[filters.annotation_count].pattern, pattern, sizeof(filters.annotation_filters[filters.annotation_count].pattern) - 1); filters.annotation_filters[filters.annotation_count].pattern[sizeof(filters.annotation_filters[filters.annotation_count].pattern) - 1] = '\0'; filters.annotation_filters[filters.annotation_count].filter_exclude_flag = (filter_type == 2) ? 1 : 0; filters.annotation_count++; printf("Annotations-Filter hinzugefügt. Gesamt: %d\n", filters.annotation_count); } else if (choice == -2) { return -2; } else if (choice == -3) { return -3; } else if (choice != -1) { printf("ERROR: Ungültige Auswahl! Bitte wählen Sie 1-8 oder b/m/q.\n"); } } return choice; } void menu_delete_filters(){ int total_filters = filters.status_count + filters.ip_count + filters.time_count + filters.user_agent_count + filters.method_count + filters.url_count + filters.annotation_flag_filter_enabled + filters.annotation_count; if (total_filters == 0) { printf("Keine Filter gesetzt zum Löschen.\n"); return; } printf("\nFILTER LÖSCHEN\n"); printf("Aktuelle Filter:\n\n"); int filter_index = 1; for (int i = 0; i < filters.status_count; i++) { char* mode_str = (filters.status_filters[i].filter_exclude_flag == 1) ? "ausschließen" : "einschließen"; printf("%2d. Status-Code: %d (%s)\n", filter_index++, filters.status_filters[i].code, mode_str); } for (int i = 0; i < filters.method_count; i++) { char* mode_str = (filters.method_filters[i].filter_exclude_flag == 1) ? "ausschließen" : "einschließen"; printf("%2d. HTTP-Methode: \"%s\" (%s)\n", filter_index++, filters.method_filters[i].pattern, mode_str); } for (int i = 0; i < filters.ip_count; i++) { char* mode_str = (filters.ip_filters[i].filter_exclude_flag == 1) ? "ausschließen" : "einschließen"; printf("%2d. IP-Adresse: %s (%s)\n", filter_index++, filters.ip_filters[i].ip_address, mode_str); } for (int i = 0; i < filters.user_agent_count; i++) { char* mode_str = (filters.user_agent_filters[i].filter_exclude_flag == 1) ? "ausschließen" : "einschließen"; printf("%2d. User-Agent: \"%s\" (%s)\n", filter_index++, filters.user_agent_filters[i].pattern, mode_str); } for (int i = 0; i < filters.url_count; i++) { char* mode_str = (filters.url_filters[i].filter_exclude_flag == 1) ? "ausschließen" : "einschließen"; printf("%2d. URL-Pfad: \"%s\" (%s)\n", filter_index++, filters.url_filters[i].pattern, mode_str); } for (int i = 0; i < filters.time_count; i++) { char* mode_str = (filters.time_filters[i].filter_exclude_flag == 1) ? "ausschließen" : "einschließen"; printf("%2d. Zeitraum: %02d.%02d.%d %02d:%02d:%02d - %02d.%02d.%d %02d:%02d:%02d (%s)\n", filter_index++, filters.time_filters[i].start_time.day, filters.time_filters[i].start_time.month, filters.time_filters[i].start_time.year, filters.time_filters[i].start_time.hour, filters.time_filters[i].start_time.minute, filters.time_filters[i].start_time.second, filters.time_filters[i].end_time.day, filters.time_filters[i].end_time.month, filters.time_filters[i].end_time.year, filters.time_filters[i].end_time.hour, filters.time_filters[i].end_time.minute, filters.time_filters[i].end_time.second, mode_str); } if (filters.annotation_flag_filter_enabled){ char* mode_str = (filters.annotation_flag_filter.filter_exclude_flag == 1) ? "ausschließen" : "einschließen"; printf("%2d. Annotierte Einträge (%s)\n", filter_index++, mode_str); } for (int i = 0; i < filters.annotation_count; i++) { char* mode_str = (filters.annotation_filters[i].filter_exclude_flag == 1) ? "ausschließen" : "einschließen"; printf("%2d. Annotations-Filter: \"%s\" (%s)\n", filter_index++, filters.annotation_filters[i].pattern, mode_str); } int choice = safe_read_integer("Filter zum Löschen auswählen (1-%d) oder 0 für Abbrechen: ", 0, total_filters); if (choice < 0) return; if (choice == 0) { return; } int current_index = 1; for (int i = 0; i < filters.status_count; i++) { if (current_index == choice) { for (int j = i; j < filters.status_count - 1; j++) { filters.status_filters[j] = filters.status_filters[j + 1]; } filters.status_count--; printf("Status-Code Filter gelöscht.\n"); return; } current_index++; } for (int i = 0; i < filters.method_count; i++) { if (current_index == choice) { for (int j = i; j < filters.method_count - 1; j++) { filters.method_filters[j] = filters.method_filters[j + 1]; } filters.method_count--; printf("Method-Filter gelöscht.\n"); return; } current_index++; } for (int i = 0; i < filters.ip_count; i++) { if (current_index == choice) { for (int j = i; j < filters.ip_count - 1; j++) { filters.ip_filters[j] = filters.ip_filters[j + 1]; } filters.ip_count--; printf("IP-Filter gelöscht.\n"); return; } current_index++; } for (int i = 0; i < filters.user_agent_count; i++) { if (current_index == choice) { for (int j = i; j < filters.user_agent_count - 1; j++) { filters.user_agent_filters[j] = filters.user_agent_filters[j + 1]; } filters.user_agent_count--; printf("User-Agent Filter gelöscht.\n"); return; } current_index++; } for (int i = 0; i < filters.url_count; i++) { if (current_index == choice) { for (int j = i; j < filters.url_count - 1; j++) { filters.url_filters[j] = filters.url_filters[j + 1]; } filters.url_count--; printf("URL-Filter gelöscht.\n"); return; } current_index++; } for (int i = 0; i < filters.time_count; i++) { if (current_index == choice) { for (int j = i; j < filters.time_count - 1; j++) { filters.time_filters[j] = filters.time_filters[j + 1]; } filters.time_count--; printf("Zeitraum-Filter gelöscht.\n"); return; } current_index++; } if (filters.annotation_flag_filter_enabled) { if (current_index == choice) { filters.annotation_flag_filter_enabled = 0; printf("Annotations-Flag-Filter gelöscht.\n"); return; } current_index++; } for (int i = 0; i < filters.annotation_count; i++) { if (current_index == choice) { for (int j = i; j < filters.annotation_count - 1; j++) { filters.annotation_filters[j] = filters.annotation_filters[j + 1]; } filters.annotation_count--; printf("Annotations-Filter gelöscht.\n"); return; } current_index++; } } void menu_reset_filters(){ printf("\nFILTER ZURÜCKSETZEN\n"); int total_filters = filters.status_count + filters.method_count + filters.ip_count + filters.time_count + filters.user_agent_count + filters.url_count + filters.annotation_flag_filter_enabled + filters.annotation_count; if (total_filters == 0) { printf("Keine Filter gesetzt zum Zurücksetzen.\n"); return; } printf("WARNING: Alle %d Filter werden gelöscht!\n\n", total_filters); printf("1. Ja, alle Filter löschen\n"); printf("2. Abbrechen\n"); printf("Navigation: [b]Zurück [m]Hauptmenü [q]Beenden\n"); printf("Auswahl: "); int choice = read_menu_input(); choice = handle_menu_shortcuts(choice); if (choice == 1) { filters.status_count = 0; filters.method_count = 0; filters.ip_count = 0; filters.time_count = 0; filters.user_agent_count = 0; filters.url_count = 0; filters.annotation_flag_filter_enabled = 0; filters.annotation_count = 0; printf("Alle Filter zurückgesetzt.\n"); } else if (choice == 2) { printf("Zurücksetzen abgebrochen.\n"); } else if (choice == -2 || choice == -3) { return; } else if (choice != -1) { printf("ERROR: Ungültige Auswahl! Bitte wählen Sie 1-2 oder b/m/q.\n"); } } void menu_filter_management(){ int choice = 0; int supress_preview = 0; // Standardnavigation aus read_menu_input while (choice != -2 && choice != -3) { if (supress_preview==0){ show_status(); } supress_preview = 0; printf("\nFILTER VERWALTEN\n"); printf("1. Filter hinzufügen\n"); printf("2. Filter löschen\n"); printf("3. Alle Filter zurücksetzen\n"); printf("4. Filter-Dokumentation anzeigen\n"); printf("Navigation: [b]Zurück [m]Hauptmenü [q]Beenden\n"); printf("Auswahl: "); choice = read_menu_input(); choice = handle_menu_shortcuts(choice); if (choice == 1) { int sub_result = menu_set_filters(); if (sub_result == -3) return; } else if (choice == 2) { menu_delete_filters(); } else if (choice == 3) { menu_reset_filters(); } else if (choice == 4) { print_filter_examples(); supress_preview = 1; } else if (choice == -2) { return; } else if (choice == -3) { return; } else if (choice != -1) { printf("ERROR: Ungültige Auswahl! Bitte wählen Sie 1-4 oder b/m/q.\n"); } } } void menu_show_entries(){ int choice = 0; int supress_preview = 0; // Standardnavigation aus read_menu_input() while (choice != -2 && choice != -3) { if (supress_preview == 0) { show_status(); } supress_preview = 0; int filtered_count = count_filtered_entries(); printf("\nDATEN ANZEIGEN UND EXPORTIEREN\n"); printf("Verfügbare Einträge: %d (gefiltert von %d)\n\n", filtered_count, total_entries); printf("1. Alle gefilterten Einträge anzeigen\n"); printf("2. Als CSV exportieren (Timesketch-kompatibel)\n"); printf("3. Top %d IP-Adressen anzeigen\n", TOP_X); printf("4. Top %d User-Agents anzeigen\n", TOP_X); printf("5. Annotierte Einträge anzeigen\n"); printf("Navigation: [b]Zurück [m]Hauptmenü [q]Beenden\n"); printf("Auswahl: "); choice = read_menu_input(); choice = handle_menu_shortcuts(choice); if (choice == 1 ) { if (filtered_count > 1000) { printf("\nWARNING: Die Anzeige von %d Einträgen in der Kommandozeile ist unübersichtlich.\n", filtered_count); printf("Empfehlung: Verwenden Sie den CSV-Export für große Datenmengen.\n\n"); printf("1. Trotzdem anzeigen\n"); printf("2. Abbrechen\n"); int confirm = safe_read_integer("Auswahl: ", 1, 2); if (confirm != 1) { printf("Anzeige abgebrochen.\n"); continue; } } supress_preview = 1; show_filtered_entries(0); } else if (choice == 2) { export_filtered_entries(NULL); } else if (choice == 3) { show_top_x_ips(); supress_preview = 1; } else if (choice == 4) { show_top_user_agents(); supress_preview = 1; } else if (choice == 5){ show_annotated_entries(); supress_preview = 1; } else if (choice == -2) { return; } else if (choice == -3) { return; } else if (choice != -1) { printf("ERROR: Ungültige Auswahl! Bitte wählen Sie 1-5 oder b/m/q.\n"); } } } // Funktionen zum setzen der Filter (existierende Datenstrukturen) void add_parsed_status_filter(char* value, int filter_exclude_flag) { if (filters.status_count >= MAX_FILTERS) { printf("WARNING: MAX_FILTERS überschritten, ignoriere: %s\n", value); return; } // Kovertierung des Statuscodes zu long mit Error handling char* endptr; int status_code = strtol(value, &endptr, 10); if (*endptr != '\0'){ printf("ERROR: Ungültiger Wert im Statuscode-Filter: %s\n", value); return; } if (status_code < 100 || status_code > 599) { printf("WARNING: Invalid status code: %s (must be 100-599)\n", value); return; } // setzen des Filters filters.status_filters[filters.status_count].code = status_code; filters.status_filters[filters.status_count].filter_exclude_flag = filter_exclude_flag; filters.status_count++; if (flag_verbose) printf("DEBUG: Filter hinzugefügt: %s%d\n", filter_exclude_flag == 1 ? "!" : "", status_code); } void add_parsed_ip_filter(char* value, int filter_exclude_flag) { if (filters.ip_count >= MAX_FILTERS) { printf("WARNING: MAX_FILTERS überschritten, ignoriere: %s\n", value); return; } // einfache Plausibilitätsprüfung hinsichtlich der Länge if (strlen(value) >= sizeof(filters.ip_filters[0].ip_address)) { printf("WARNING: IP-Adresse zu lang: %s\n", value); return; } // setzen des Filters strncpy(filters.ip_filters[filters.ip_count].ip_address, value, sizeof(filters.ip_filters[filters.ip_count].ip_address) - 1); filters.ip_filters[filters.ip_count].ip_address[sizeof(filters.ip_filters[filters.ip_count].ip_address) - 1] = '\0'; filters.ip_filters[filters.ip_count].filter_exclude_flag = filter_exclude_flag; filters.ip_count++; if (flag_verbose) printf("DEBUG: IP-Adressfilter hinzugefügt: %s%s\n", filter_exclude_flag == 1 ? "!" : "", value); } // gleiche Mechanik wie bei IP-Adresse void add_parsed_method_filter(char* value, int filter_exclude_flag) { if (filters.method_count >= MAX_FILTERS) { printf("WARNING: MAX_FILTERS überschritten, ignoriere: %s\n", value); return; } if (strlen(value) >= sizeof(filters.method_filters[0].pattern)) { printf("WARNING: Methoden-Filterwert zu lang: %s\n", value); return; } strncpy(filters.method_filters[filters.method_count].pattern, value, sizeof(filters.method_filters[filters.method_count].pattern) - 1); filters.method_filters[filters.method_count].pattern[sizeof(filters.method_filters[filters.method_count].pattern) - 1] = '\0'; filters.method_filters[filters.method_count].filter_exclude_flag = filter_exclude_flag; filters.method_count++; if (flag_verbose) printf("DEBUG: Methoden-Filter hinzugefügt: %s%s\n", filter_exclude_flag == 1 ? "!" : "", value); } // gleiche Mechanik wie bei IP-Adresse void add_parsed_useragent_filter(char* value, int filter_exclude_flag) { if (filters.user_agent_count >= MAX_FILTERS) { printf("WARNING: MAX_FILTERS überschritten, ignoriere: %s\n", value); return; } if (strlen(value) >= sizeof(filters.user_agent_filters[0].pattern)) { printf("WARNING: User agent Filterwert zu lang: %s\n", value); return; } strncpy(filters.user_agent_filters[filters.user_agent_count].pattern, value, sizeof(filters.user_agent_filters[filters.user_agent_count].pattern) - 1); filters.user_agent_filters[filters.user_agent_count].pattern[sizeof(filters.user_agent_filters[filters.user_agent_count].pattern) - 1] = '\0'; filters.user_agent_filters[filters.user_agent_count].filter_exclude_flag = filter_exclude_flag; filters.user_agent_count++; if (flag_verbose) printf("DEBUG: User Agent Filter hinzugefügt: %s%s\n", filter_exclude_flag == 1 ? "!" : "", value); } // gleiche Mechanik wie bei IP-Adresse void add_parsed_url_filter(char* value, int filter_exclude_flag) { if (filters.url_count >= MAX_FILTERS) { printf("WARNING: MAX_FILTERS überschritten, ignoriere: %s\n", value); return; } if (strlen(value) >= sizeof(filters.url_filters[0].pattern)) { printf("WARNING: URL/Payload Filterwert zu lang: %s\n", value); return; } strncpy(filters.url_filters[filters.url_count].pattern, value, sizeof(filters.url_filters[filters.url_count].pattern) - 1); filters.url_filters[filters.url_count].pattern[sizeof(filters.url_filters[filters.url_count].pattern) - 1] = '\0'; filters.url_filters[filters.url_count].filter_exclude_flag = filter_exclude_flag; filters.url_count++; if (flag_verbose) printf("DEBUG: URL/Payload-Filter hinzugefügt: %s%s\n", filter_exclude_flag == 1 ? "!" : "", value); } // Parsen des Timestamp-Filters ist etwas komplexer, da er in die Datenstruktur geschrieben werden muss. void add_parsed_timerange_filter(char* value, int filter_exclude_flag) { if (filters.time_count >= MAX_FILTERS) { printf("WARNING: MAX_FILTERS überschritten, ignoriere: %s\n", value); return; } //lokale Kopie struct time_filter_t new_time_filter = {0}; //Zur Position des :, der die Startzeit von der Endzeit trennt char* colon_pos = strchr(value, ':'); if (colon_pos == NULL) { printf("ERROR: Missing ':' separator in timerange filter\n"); return; } // ERsteze : durch Nullterminator. Das ist notwendig, damit strtok den Timestamp problemlos einlesen kann *colon_pos = '\0'; //Für jeden Datenpunkt: Einlesen bis zum -, sowie Error handling, da atoi gar nix macht, wenn das Token nicht geparsed werden konnte char* token = strtok(value, "-"); if (!token) return; int start_year = atoi(token); if (start_year < 1970 || start_year > 2100) { printf("ERROR: Startjahr außerhalb des möglichen Bereichs: %d\n", start_year); return; } token = strtok(NULL, "-"); if (!token) return; int start_month = atoi(token); if (start_month < 1 || start_month > 12) { printf("ERROR: Startmonat außerhalb des möglichen Bereichs: %d\n", start_month); return; } token = strtok(NULL, "-"); if (!token) return; int start_day = atoi(token); if (start_day < 1 || start_day > 31) { printf("ERROR: Starttag außerhalb des möglichen Bereichs: %d\n", start_day); return; } token = strtok(NULL, "-"); if (!token) return; int start_hour = atoi(token); if (start_hour < 0 || start_hour > 23) { printf("ERROR: Startstunde außerhalb des möglichen Bereichs: %d\n", start_hour); return; } token = strtok(NULL, "-"); if (!token) return; int start_minute = atoi(token); if (start_minute < 0 || start_minute > 59) { printf("ERROR: Startminute außerhalb des möglichen Bereichs: %d\n", start_minute); return; } token = strtok(NULL, "-"); if (!token) return; int start_second = atoi(token); if (start_second < 0 || start_second > 59) { printf("ERROR: Startsekunde außerhalb des möglichen Bereichs: %d\n", start_second); return; } token = strtok(colon_pos+1, "-"); if (!token) return; int end_year = atoi(token); if (end_year < 1970 || end_year > 2100) { printf("ERROR: Endjahr außerhalb des möglichen Bereichs: %d\n", end_year); return; } token = strtok(NULL, "-"); if (!token) return; int end_month = atoi(token); if (end_month < 1 || end_month > 12) { printf("ERROR: Endmonat außerhalb des möglichen Bereichs: %d\n", end_month); return; } token = strtok(NULL, "-"); if (!token) return; int end_day = atoi(token); if (end_day < 1 || end_day > 31) { printf("ERROR: Endtag außerhalb des möglichen Bereichs: %d\n", end_day); return; } token = strtok(NULL, "-"); if (!token) return; int end_hour = atoi(token); if (end_hour < 0 || end_hour > 23) { printf("ERROR: Endstunde außerhalb des möglichen Bereichs: %d\n", end_hour); return; } token = strtok(NULL, "-"); if (!token) return; int end_minute = atoi(token); if (end_minute < 0 || end_minute > 59) { printf("ERROR: Endminute außerhalb des möglichen Bereichs: %d\n", end_minute); return; } token = strtok(NULL, "-"); if (!token) return; int end_second = atoi(token); if (end_second < 0 || end_second > 59) { printf("ERROR: Endsekunde außerhalb des möglichen Bereichs: %d\n", end_second); return; } // integer-Variablen in die lokale Datenstruktur schreiben new_time_filter.start_time.year = start_year; new_time_filter.start_time.month = start_month; new_time_filter.start_time.day = start_day; new_time_filter.start_time.hour = start_hour; new_time_filter.start_time.minute = start_minute; new_time_filter.start_time.second = start_second; new_time_filter.end_time.year = end_year; new_time_filter.end_time.month = end_month; new_time_filter.end_time.day = end_day; new_time_filter.end_time.hour = end_hour; new_time_filter.end_time.minute = end_minute; new_time_filter.end_time.second = end_second; new_time_filter.filter_exclude_flag = filter_exclude_flag; // Logik hier: der neue Filter wird stets an einen neuen Index geschrieben. Dieser ergibt sich aus der aktuellen ANZAHL der existierenden Zeitfilter. Das funktioniert, weil der index bei 0 beginnt filters.time_filters[filters.time_count] = new_time_filter; filters.time_count++; if (flag_verbose) printf("DEBUG: Zeitraum-Filter hinzugefügt (%s): %04d-%02d-%02d %02d:%02d:%02d bis %04d-%02d-%02d %02d:%02d:%02d\n", filter_exclude_flag == 1 ? "ausschließen" : "einschließen", new_time_filter.start_time.year, new_time_filter.start_time.month, new_time_filter.start_time.day, new_time_filter.start_time.hour, new_time_filter.start_time.minute, new_time_filter.start_time.second, new_time_filter.end_time.year, new_time_filter.end_time.month, new_time_filter.end_time.day, new_time_filter.end_time.hour, new_time_filter.end_time.minute, new_time_filter.end_time.second); } // recht einfache Mechanik für den Bool´schen Filter für annotierte Einträge void add_parsed_annotated_filter(char* value, int filter_exclude_flag) { if (strcmp(value, "true") == 0) { filters.annotation_flag_filter.annotation_flag_is_present = 1; filters.annotation_flag_filter.filter_exclude_flag = filter_exclude_flag; filters.annotation_flag_filter_enabled = 1; if (flag_verbose) printf("DEBUG: Annotations-Flag-Filter hinzugefügt: %s%s\n", filter_exclude_flag == 1 ? "!" : "", value); // falls doch mal jemand false eingibt } else if (strcmp(value, "false") == 0) { filters.annotation_flag_filter.annotation_flag_is_present = 0; filters.annotation_flag_filter.filter_exclude_flag = filter_exclude_flag; filters.annotation_flag_filter_enabled = 1; if (flag_verbose) printf("DEBUG: Annotations-Flag-Filter hinzugefügt: %s%s\n", filter_exclude_flag == 1 ? "!" : "", value); } else { printf("WARNING: Ungültiger Wert für --annotated Filter: %s (nur 'true' unterstützt)\n", value); } } // Wieder gleiche Mechanik wie beim User Agent etc void add_parsed_annotation_filter(char* value, int filter_exclude_flag) { if (filters.annotation_count >= MAX_FILTERS) { printf("WARNING: MAX_FILTERS überschritten, ignoriere: %s\n", value); return; } if (strlen(value) >= sizeof(filters.annotation_filters[0].pattern)) { printf("WARNING: Annotations-Filterwert zu lang: %s\n", value); return; } strncpy(filters.annotation_filters[filters.annotation_count].pattern, value, sizeof(filters.annotation_filters[filters.annotation_count].pattern) - 1); filters.annotation_filters[filters.annotation_count].pattern[sizeof(filters.annotation_filters[filters.annotation_count].pattern) - 1] = '\0'; filters.annotation_filters[filters.annotation_count].filter_exclude_flag = filter_exclude_flag; filters.annotation_count++; if (flag_verbose) printf("DEBUG: Annotations-Filter hinzugefügt: %s%s\n", filter_exclude_flag == 1 ? "!" : "", value); } // Funktion zum Parsen der Filter-Werte, die mit --= übergeben werden. // values_str sind die Werte hinter dem =, filter_type die Werte vor dem = // filter_type wird von parse_filter_argument() übergeben void parse_filter_values(char* values_str, char* filter_type) { char values_local[1024]; // Werte in lokale Variable einlesen, Nullterminator setzen strncpy(values_local, values_str, sizeof(values_local) - 1); values_local[sizeof(values_local) - 1] = '\0'; // Einlesen der Werte mit strtok. Hier wird das erste Token gesetzt, damit die exit condition der while-Schleife nicht triggert. // strtok liest einen string in einen Pointer bis zu einem spezifizierten Delimiter ein. Die gelesenen Bytes werden aus dem Input entfernt. char* token = strtok(values_local, ","); while (token != NULL) { // Sollte der Nutzer Leerzeichen verwendet haben, müssen diese übersprungen werden while (*token == ' ') token++; // setzen des Modus, Standard inklusiv int filter_exclude_flag = 0; // ..mit !-Präfix exklusiv. if (*token == '!') { filter_exclude_flag = 1; token++; // den Pointer vom ! weiteriterieren, dieser ist nicht Teil des Filters } if (strlen(token) > 0) { // wenn das token Werte hat, werden die entsprechenden Funktionen aufgerufen, die die Filter setzen if (strcmp(filter_type, "status") == 0) { add_parsed_status_filter(token, filter_exclude_flag); } else if (strcmp(filter_type, "ip") == 0) { add_parsed_ip_filter(token, filter_exclude_flag); } else if (strcmp(filter_type, "method") == 0) { add_parsed_method_filter(token, filter_exclude_flag); } else if (strcmp(filter_type, "useragent") == 0) { add_parsed_useragent_filter(token, filter_exclude_flag); } else if (strcmp(filter_type, "url") == 0) { add_parsed_url_filter(token, filter_exclude_flag); } else if (strcmp(filter_type, "timerange") == 0) { add_parsed_timerange_filter(token, filter_exclude_flag); } else if (strcmp(filter_type, "annotated") == 0) { add_parsed_annotated_filter(token, filter_exclude_flag); } else if (strcmp(filter_type, "annotation") == 0) { add_parsed_annotation_filter(token, filter_exclude_flag); } } // nächste Iteration - nächstes token einlesen, damit die while-Schleife weiteriteriert token = strtok(NULL, ","); } } // Filter-Argument Parser int parse_filter_argument(char* arg) { if (!starts_with(arg, "--")) { return 0; } // = finden, strchr gibt den Pointer auf das = zurück char* equals_pos = strchr(arg, '='); if (equals_pos == NULL) { printf("WARNING: Ungültiges Filter-Format, kein '=' gefunden: %s\n", arg); return 0; } // filter-Typ parsen: 1. Länge des Strings errechnen (Position des '=' abzüglich Startposition des Strings, abzüglich 2 (für die '--' am Anfang)) int filterstr_length = equals_pos - arg -2; // 2. char filter_type[50]; strncpy(filter_type, arg +2, filterstr_length); filter_type[filterstr_length] = '\0'; // Filter-Werte (nach den =) char* values = equals_pos + 1; if (strstr(filter_type, "status") != NULL) { parse_filter_values(values, "status"); } else if (strstr(filter_type, "ip") != NULL) { parse_filter_values(values, "ip"); } else if (strstr(filter_type, "method") != NULL) { parse_filter_values(values, "method"); } else if (strstr(filter_type, "useragent") != NULL) { parse_filter_values(values, "useragent"); } else if (strstr(filter_type, "url") != NULL) { parse_filter_values(values, "url"); } else if (strstr(filter_type, "timerange") != NULL) { parse_filter_values(values, "timerange"); } else if (strstr(filter_type, "annotated") != NULL) { parse_filter_values(values, "annotated"); } else if (strstr(filter_type, "annotation") != NULL) { parse_filter_values(values, "annotation"); } else { printf("WARNING: Unbekannter Filtertyp: %s\n", filter_type); return 0; } return 1; } // DISCLAIMER: Updated and improved from original version void print_help(char* binary) { printf("\nNGINX EXAMINATOR - Improved Version\n"); printf("Verwendung:\n"); printf(" %s -i Interaktiver Modus (Filter/Analyse/Export)\n", binary); printf(" %s -e Export generieren\n", binary); printf(" %s -f [FILTER...] Mit Kommandozeilen-Filtern\n", binary); printf(" %s -h Diese Hilfe anzeigen\n", binary); printf("\nArgumente:\n"); printf(" Pfad zu einer einzelnen *.log Datei ODER zu einem Ordner,\n"); printf(" der eine oder mehrere *.log / *.log.N Dateien enthält.\n"); printf("\nFlags:\n"); printf(" -i Startet interaktiven Modus mit Filtern (Status, Methode, IP, Zeitraum,\n"); printf(" User-Agent, URL-Teilstring, Annotationen), Vorschau, Statistiken und CSV-Export (Timesketch).\n"); printf(" -e Generiert Timesketch-kompatible CSV-Datei, nimmt optional Dateinamen entgegen.\n"); printf(" -f Aktiviert Kommandozeilen-Filter. Muss von Filter-Argumenten gefolgt werden.\n"); printf(" -v Verbose-Modus für detaillierte Debug-Informationen.\n"); printf(" -h Zeigt diese Hilfe an.\n"); printf("\nKOMMANDOZEILEN-FILTER (nur mit -f Flag):\n"); printf(" --status=CODE[,CODE...] HTTP-Status-Codes (z.B. 200,404,500)\n"); printf(" --ip=ADRESSE[,ADRESSE...] IP-Adressen (exakte Übereinstimmung)\n"); printf(" --method=METHODE[,METHODE...] HTTP-Methoden (z.B. GET,POST,ATYPICAL)\n"); printf(" --useragent=TEXT[,TEXT...] User-Agent Teilstrings (z.B. bot,crawler)\n"); printf(" --url=PFAD[,PFAD...] URL-Pfad Teilstrings (z.B. .git,.php,wp-)\n"); printf(" --annotated=true Einträge mit/ohne Annotationen (true oder !true)\n"); printf(" --annotation=TEXT[,TEXT...] Annotations-Teilstrings (z.B. 'Long Payload')\n"); printf(" --timerange=START:END[,...] Zeiträume (Format: YYYY-MM-DD-HH-MM-SS:YYYY-MM-DD-HH-MM-SS)\n"); printf("\nFILTER-SYNTAX:\n"); printf(" Einschluss: Wert (nur Einträge MIT diesem Wert)\n"); printf(" Ausschluss: !Wert (alle Einträge OHNE diesen Wert)\n"); printf(" Mehrere: Wert1,Wert2 (kommagetrennt, keine Leerzeichen)\n"); printf(" Gemischt: Wert1,!Wert2 (Wert1 einschließen, Wert2 ausschließen)\n"); printf("\nZEITRAUM-SYNTAX:\n"); printf(" Format: YYYY-MM-DD-HH-MM-SS:YYYY-MM-DD-HH-MM-SS\n"); printf(" Beispiel: 2025-08-31-08-00-00:2025-08-31-18-00-00\n"); printf(" Ausschluss: !2025-08-31-02-00-00:2025-08-31-06-00-00\n"); printf(" Mehrere: Start1:End1,!Start2:End2,Start3:End3\n"); printf("\nFILTER-LOGIK (Vereinfacht):\n"); printf(" - Ausschluss-Filter (!) haben IMMER Vorrang vor Einschluss-Filtern\n"); printf(" - UND-Verknüpfung zwischen verschiedenen Filtertypen (IP UND Status UND Methode...)\n"); printf(" - ODER-Verknüpfung innerhalb gleicher Filtertypen (Status 200 ODER 404)\n"); printf(" - Einfache, vorhersagbare Logik ohne komplexe Modi\n"); printf("\nUnterstützte Eingaben:\n"); printf(" - Normale NGINX-Access-Logs: *.log\n"); printf(" - Rotierte Logs: *.log.1, *.log.2, ... (rein textbasiert)\n"); printf(" - GZIP-Archive (*.gz) werden automatisch dekomprimiert\n"); printf("\nErwartetes NGINX-Logformat (default `log_format main`):\n"); printf(" log_format main '$remote_addr - $remote_user [$time_local] \"$request\" '\n"); printf(" '$status $body_bytes_sent \"$http_referer\" '\n"); printf(" '\"$http_user_agent\" \"$http_x_forwarded_for\"';\n"); printf("\nBeispielzeile (eine Zeile pro Request):\n"); printf(" 0.0.0.0 - - [31/Aug/2025:00:11:42 +0000] \"GET /.git/config HTTP/1.1\" 400 255 \"-\" \"Mozilla/5.0\" \"-\"\n"); printf("\nCSV-Export (Timesketch-kompatibel):\n"); printf(" Spalten: datetime, message timestamp_desc, ip_address, method, url_path, status_code,\n"); printf(" bytes_sent, user_agent, source_file, parsing_timestamp, tag\n"); printf("\nAnnotationen (Automatische Erkennung):\n"); printf(" - Automatische Erkennung verdächtiger Muster (z.B. 'Long Payload', 'SQL Injection')\n"); printf(" - Filterbar über --annotated und --annotation Parameter\n"); printf(" - Sichtbar in interaktivem Modus und CSV-Export\n"); printf(" - Über 13 verschiedene Angriffsmuster werden erkannt\n"); printf("\nHinweise & Verbesserungen:\n"); printf(" - Parser erwartet das obige Standardformat. Abweichungen können zum Abbruch führen.\n"); printf(" - Verbesserte Speichersicherheit mit strncpy statt strcpy\n"); printf(" - Vereinfachte, vorhersagbare Filterlogik ohne komplexe AND/OR-Modi\n"); printf(" - Umfassende interaktive Benutzeroberfläche mit Navigation\n"); printf(" - Automatische Anomalie-Erkennung für Sicherheitsanalysen\n"); printf(" - ATYPICAL-Methode: für fehlerhafte/binäre Requests innerhalb der \"request\"-Spalte.\n"); } int main(int argc, char* argv[]) { if (argc < 2) { print_help(argv[0]); return 1; } printf("\nNGINX EXAMINATOR - Advanced Log Analysis Tool\n"); // Hybrid argument parsing: support both path-first and flag-first approaches char* input_path = "/var/log/nginx"; // Default path int arg_offset = 1; // Check if first argument is a path (doesn't start with -) if (argv[1][0] != '-') { input_path = argv[1]; arg_offset = 2; // Start parsing flags from argv[2] if (flag_verbose) printf("DEBUG: Using path from first argument: %s\n", input_path); } else { arg_offset = 1; // Start parsing flags from argv[1] if (flag_verbose) printf("DEBUG: Using default path: %s\n", input_path); } // Flag variables int flag_interactive = 0; int flag_export = 0; int flag_help = 0; int flag_filter = 0; char export_filename[90]; int flag_has_filename = 0; allocate_initial_memory(); // Parse arguments starting from calculated offset for (int i = arg_offset; i < argc; i++) { if (strcmp(argv[i], "-i") == 0) { flag_interactive = 1; // Check if next argument is a path (for -i /path/to/logs pattern) if (i + 1 < argc && argv[i + 1][0] != '-') { input_path = argv[i + 1]; i++; // Skip the path argument in next iteration if (flag_verbose) printf("DEBUG: Path set via -i flag: %s\n", input_path); } } else if (strcmp(argv[i], "-e") == 0) { flag_export = 1; // Check for optional filename argument if (i + 1 < argc && argv[i + 1][0] != '-') { strncpy(export_filename, argv[i + 1], sizeof(export_filename) - 1); export_filename[sizeof(export_filename) - 1] = '\0'; flag_has_filename = 1; i++; // Skip the filename argument in next iteration } } else if (strcmp(argv[i], "-f") == 0) { flag_filter = 1; // Parse subsequent filter arguments for (int j = i + 1; j < argc; j++) { if (starts_with(argv[j], "--")) { // Check for --input flag to override path if (starts_with(argv[j], "--input=")) { char* equals_pos = strchr(argv[j], '='); if (equals_pos != NULL) { input_path = equals_pos + 1; if (flag_verbose) printf("DEBUG: Path overridden via --input flag: %s\n", input_path); } } else { parse_filter_argument(argv[j]); } } else { break; // Stop parsing filters when we hit a non-filter argument } } } else if (starts_with(argv[i], "--input=")) { // Handle --input flag outside of -f context char* equals_pos = strchr(argv[i], '='); if (equals_pos != NULL) { input_path = equals_pos + 1; if (flag_verbose) printf("DEBUG: Path set via --input flag: %s\n", input_path); } } else if (starts_with(argv[i], "--")) { // Handle filter arguments that might be used without -f flag parse_filter_argument(argv[i]); flag_filter = 1; // Implicit filter mode } else if (strcmp(argv[i], "-v") == 0) { flag_verbose = 1; } else if (strcmp(argv[i], "-h") == 0) { flag_help = 1; } } // Handle help flag first if (flag_help == 1) { print_help(argv[0]); cleanup_memory(); return 0; } // Validate flag combinations int active_modes = flag_interactive + flag_export + flag_filter; if (active_modes > 1) { printf("ERROR: Nur ein Hauptmodus kann gleichzeitig verwendet werden (-i, -e, oder -f).\n"); printf("Verwenden Sie %s -h für Details.\n", argv[0]); cleanup_memory(); return 1; } // Execute based on selected mode if (flag_interactive == 1) { // Interactive mode load_log_file(input_path); if (total_entries == 0) { printf("ERROR: Keine gültigen Log-Einträge gefunden. Überprüfen Sie den Pfad und die Dateiformate.\n"); cleanup_memory(); return 1; } int choice = 0; while (choice != 4 && choice != -4) { show_status(); show_main_menu(); choice = read_menu_input(); choice = handle_menu_shortcuts(choice); if (choice == -1) { printf("ERROR: Bitte geben Sie eine gültige Zahl oder Navigation ein (1-4, b, m, q)!\n"); continue; } if (choice == 1) { menu_filter_management(); } else if (choice == 2) { menu_show_entries(); } else if (choice == 3) { export_filtered_entries(NULL); } else if (choice == 4) { if (flag_verbose) printf("DEBUG: Programmende\n"); break; } else if (choice == -4) { break; } else { printf("ERROR: Ungültige Auswahl! Bitte wählen Sie 1-4 oder b/m/q.\n"); } } } else if (flag_export == 1) { // Export mode load_log_file(input_path); if (flag_has_filename == 1) { export_filtered_entries(export_filename); } else { export_filtered_entries(NULL); } } else if (flag_filter == 1) { // Filter mode - apply command line filters and show summary load_log_file(input_path); int filtered_count = count_filtered_entries(); printf("\nFILTER-ERGEBNISSE:\n"); printf("Geladen: %d Einträge\n", total_entries); printf("Gefiltert: %d Einträge entsprechen den Kriterien\n", filtered_count); printf("Verdächtige Muster: %d automatisch erkannt\n\n", suspicious_patterns_count); if (filtered_count > 0) { printf("Verwenden Sie -e für CSV-Export oder -i für interaktive Analyse.\n"); } } else { // Default mode - simple summary load_log_file(input_path); int filtered_count = count_filtered_entries(); printf("\nZUSAMMENFASSUNG:\n"); printf("Geladen: %d Einträge aus %s\n", total_entries, input_path); printf("Verdächtige Muster automatisch erkannt: %d\n", suspicious_patterns_count); printf("\nVerfügbare Modi:\n"); printf(" -i Interaktiver Modus für detaillierte Analyse\n"); printf(" -e CSV-Export für Timesketch\n"); printf(" -f Kommandozeilen-Filter anwenden\n"); printf(" -h Vollständige Hilfe anzeigen\n"); } cleanup_memory(); return 0; }