diff --git a/README.md b/README.md index 346915f..55eb82e 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,11 @@ # nginx-beleg +Projekt Programmierung I - nginx webserver Log Parser + +Beispiellogs unter +https://cloud.cc24.dev/s/Zexs2NS3EDYNzj2 +abrufbar. + +**ACHTUNG** +Einschränkungen: +- Kann derzeit nur accesslogs und redirect-accesslogs verarbeiten diff --git a/bin/main b/bin/main new file mode 100755 index 0000000..67c5420 Binary files /dev/null and b/bin/main differ diff --git a/lib/gzunpack.c b/lib/gzunpack.c new file mode 100644 index 0000000..2494f10 --- /dev/null +++ b/lib/gzunpack.c @@ -0,0 +1,242 @@ +/* ---------------------------------------------------------------- +# This source code is an example from my CodeBlog: +# http://codeblog.vurdalakov.net +# +# Copyright (c) 2011 Vurdalakov +# http://www.vurdalakov.net +# vurdalakov@gmail.com +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation +# files (the "Software"), to deal in the Software without +# restriction, including without limitation the rights to use, +# copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the +# Software is furnished to do so, subject to the following +# conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +# OTHER DEALINGS IN THE SOFTWARE. +---------------------------------------------------------------- */ + +#include "gzunpack.h" + +#include +#include + +#include "zlib.h" + +int readBuffer(FILE* fileHandle, void* buffer, size_t bytesToRead) +{ + size_t bytesRead; + + bytesRead = fread(buffer, 1, bytesToRead, fileHandle); + + if (ferror(fileHandle)) { + return 0; + } + + if (bytesRead != bytesToRead) { + return 0; + } + + return 1; +} + +char* readZeroTerminatedString(FILE* fileHandle) +{ + long filePosition; + int length; + unsigned char byte; + char* string; + + filePosition = ftell(fileHandle); + + if (-1 == filePosition) { + return 0; + } + + length = 0; + while (1) { + byte = fgetc(fileHandle); + if (ferror(fileHandle)) { + return 0; + } + + length++; + + if (0 == byte) { + break; + } + } + + if (fseek(fileHandle, filePosition, SEEK_SET)) { + return 0; + } + + string = malloc(length); + + length = 0; + while (1) { + byte = fgetc(fileHandle); + if (ferror(fileHandle)) { + return 0; + } + + string[length] = byte; + + if (0 == byte) { + break; + } + + length++; + } + + return string; +} + +GzFileHeader *gzUnpackHeader(FILE *packedFileHandle) +{ + unsigned short signature; + GzFileHeader* gzFileHeader; + + if (!readBuffer(packedFileHandle, &signature, 2)) { + return 0; + } + + if (signature != 0x8B1F) { + return 0; + } + + gzFileHeader = malloc(sizeof(GzFileHeader)); + memset(gzFileHeader, 0, sizeof(GzFileHeader)); + + if (!readBuffer(packedFileHandle, &gzFileHeader->compressionMethod, 1)) { + return 0; + } + + if (!readBuffer(packedFileHandle, &gzFileHeader->flags, 1)) { + return 0; + } + + if (!readBuffer(packedFileHandle, &gzFileHeader->modificationTime, 4)) { + return 0; + } + + if (!readBuffer(packedFileHandle, &gzFileHeader->extraFlags, 1)) { + return 0; + } + + if (!readBuffer(packedFileHandle, &gzFileHeader->operatingSystem, 1)) { + return 0; + } + + if (gzFileHeader->flags & 0x04) { + if (!readBuffer(packedFileHandle, &gzFileHeader->extraFieldLength, 2)) { + return 0; + } + + if (!readBuffer(packedFileHandle, &gzFileHeader->extraField, gzFileHeader->extraFieldLength)) { + return 0; + } + } + + if (gzFileHeader->flags & 0x08) { + gzFileHeader->originalFileName = readZeroTerminatedString(packedFileHandle); + } + + if (gzFileHeader->flags & 0x10) { + gzFileHeader->fileComment = readZeroTerminatedString(packedFileHandle); + } + + if (gzFileHeader->flags & 0x02) { + if (!readBuffer(packedFileHandle, &gzFileHeader->headerCrc, 2)) { + return 0; + } + } + + return gzFileHeader; +} + +void gzFreeHeader(GzFileHeader *gzFileHeader) +{ + if (0 == gzFileHeader) { + return; + } + + free(gzFileHeader->extraField); + free(gzFileHeader->originalFileName); + free(gzFileHeader->fileComment); + + free(gzFileHeader); +} + +int gzUnpackFile(FILE *packedFileHandle, FILE *unpackedFileHandle) +{ + z_stream stream; + int result; + unsigned char input_buffer[65536]; + unsigned char output_buffer[65536]; + unsigned int size; + + stream.zalloc = 0; + stream.zfree = 0; + stream.opaque = 0; + stream.next_in = 0; + stream.avail_in = 0; + + result = inflateInit2(&stream, 16 + MAX_WBITS); + if (result != Z_OK) { + return 1; + } + + do + { + stream.avail_in = fread(input_buffer, 1, sizeof(input_buffer), packedFileHandle); + if (ferror(packedFileHandle)) { + (void)inflateEnd(&stream); + return 2; + } + if (0 == stream.avail_in) { + break; + } + stream.next_in = input_buffer; + + do { + stream.avail_out = sizeof(output_buffer); + stream.next_out = output_buffer; + + result = inflate(&stream, Z_NO_FLUSH); + switch (result) + { + case Z_NEED_DICT: + result = Z_DATA_ERROR; + case Z_STREAM_ERROR: + case Z_DATA_ERROR: + case Z_MEM_ERROR: + (void)inflateEnd(&stream); + return 3; + } + + size = sizeof(output_buffer) - stream.avail_out; + if ((fwrite(output_buffer, 1, size, unpackedFileHandle) != size) || ferror(unpackedFileHandle)) { + (void)inflateEnd(&stream); + return 4; + } + } while (0 == stream.avail_out); + + } while (result != Z_STREAM_END); + + inflateEnd(&stream); + + return 0; +} \ No newline at end of file diff --git a/lib/gzunpack.h b/lib/gzunpack.h new file mode 100644 index 0000000..94ec54e --- /dev/null +++ b/lib/gzunpack.h @@ -0,0 +1,56 @@ +/* ---------------------------------------------------------------- +# This source code is an example from my CodeBlog: +# http://codeblog.vurdalakov.net +# +# Copyright (c) 2011 Vurdalakov +# http://www.vurdalakov.net +# vurdalakov@gmail.com +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation +# files (the "Software"), to deal in the Software without +# restriction, including without limitation the rights to use, +# copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the +# Software is furnished to do so, subject to the following +# conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +# OTHER DEALINGS IN THE SOFTWARE. +---------------------------------------------------------------- */ + +#ifndef GZUNPACK_H +#define GZUNPACK_H + +#include + +typedef struct +{ + unsigned char compressionMethod; + unsigned char flags; + unsigned long modificationTime; + unsigned char extraFlags; + unsigned char operatingSystem; + unsigned short extraFieldLength; + unsigned char *extraField; + char *originalFileName; + char *fileComment; + unsigned short headerCrc; +} GzFileHeader; + +GzFileHeader *gzUnpackHeader(FILE *packedFileHandle); + +void gzFreeHeader(GzFileHeader *gzFileHeader); + +int gzUnpackFile(FILE *packedFileHandle, FILE *unpackedFileHandle); + +#endif \ No newline at end of file diff --git a/src/main-prod.c b/src/main-prod.c new file mode 100644 index 0000000..813acf5 --- /dev/null +++ b/src/main-prod.c @@ -0,0 +1,3201 @@ +/* +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 { + int day; + int month; + int year; + int hour; + int minute; + int second; +}; + +// Struktur für die Darstellung eines Standard-NGINX-Logeintrags. +struct log_entry { + 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 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 { + int code; + int filter_exclude_flag; +}; + +struct method_filter { + char pattern[10]; + int filter_exclude_flag; +}; + +// für IP-Adressen +struct ip_filter { + char ip_address[50]; + int filter_exclude_flag; +}; + +// für Zeit, Start- und Endzeit +struct time_filter { + struct simple_time start_time; + struct simple_time end_time; + int filter_exclude_flag; +}; + +// Filter für User-Agent +struct user_agent_filter { + char pattern[256]; + int filter_exclude_flag; +}; + +// Filter für URL-Pfad/Request +struct url_filter { + char pattern[MAX_REQUEST_LENGTH]; + int filter_exclude_flag; +}; + +struct annotation_flag_filter { + int annotation_flag_is_present; + int filter_exclude_flag; +}; + +struct annotation_filter { + char pattern[64]; + int filter_exclude_flag; +}; + +// Struktur zum erhalten aller Filtereinträge, kann im Dialogbetrieb bearbeitet werden. Mit Zähler. +struct filter_system { + struct status_filter status_filters[MAX_FILTERS]; + int status_count; + + struct method_filter method_filters[MAX_FILTERS]; + int method_count; + + struct ip_filter ip_filters[MAX_FILTERS]; + int ip_count; + + struct time_filter time_filters[MAX_FILTERS]; + int time_count; + + struct user_agent_filter user_agent_filters[MAX_FILTERS]; + int user_agent_count; + + struct url_filter url_filters[MAX_FILTERS]; + int url_count; + + struct annotation_flag_filter annotation_flag_filter; + int annotation_flag_filter_enabled; + + struct annotation_filter annotation_filters[MAX_FILTERS]; + int annotation_count; + + int combination_mode; // 0=AND-Filter oder 1=OR-Filter +}; + +// Definition einer Datenstruktur für die IP-Adressen Topliste +struct ip_stat { + char ip_address[50]; + int count; +}; + +// Initialisierung eines Arrays für die Logeinträge und weiterer Startvariablen +struct log_entry *all_entries = NULL; +int max_entries = 0; +int total_entries = 0; +int suspicious_patterns_count = 0; +struct filter_system 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 time_checkvalue, struct simple_time 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))); + 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 *new_ptr = realloc(all_entries, max_entries * sizeof(struct log_entry)); + + 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))); + 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))); + } +} + +void allocate_initial_memory(){ + max_entries = INITIAL_ENTRIES; // Startwert 1000, globale Variable + all_entries = malloc(max_entries * sizeof(struct log_entry)); + + 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))); + 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))); +} + +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* 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 + strcpy(all_entries[entry_index].request_method, temp_method); + 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 + strcpy(all_entries[entry_index].request_method, "ATYPICAL"); + + // 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 "", wird übersprungen da nicht gespeichert + if (*current_pos == '"') { + current_pos++; // öffnendes Anführungszeichen überspringen + // Referrer-Inhalt bis zum schließenden Anführungszeichen überspringen + while (*current_pos != '"' && *current_pos != '\0') { + current_pos++; + } + 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)), max_entries); +} + +// 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) { + if (filters.user_agent_count == 0) return 1; + // Ausschluss-Filter geht vor + for (int i = 0; i < filters.user_agent_count; i++) { + if (filters.user_agent_filters[i].filter_exclude_flag == 1) { + int pattern_found = search_in_string(user_agent, filters.user_agent_filters[i].pattern); + if (pattern_found) { + return 0; // früheres Verlassen der Schleife, sobald Ausschlussfilter zutrifft + } + } + } + // Prüfung im entsprechenden Modus + int include_count = 0; + int include_matches = 0; + + for (int i = 0; i < filters.user_agent_count; i++) { + if (filters.user_agent_filters[i].filter_exclude_flag == 0) { + include_count++; + int pattern_found = search_in_string(user_agent, filters.user_agent_filters[i].pattern); + if (pattern_found) { + include_matches++; + if (filters.combination_mode == 1) { // OR-Modus + return 1; // Früheres Verlassen der Schleife bei erstem zutreffendem Einschlussfilter im OR-Modus + } + } + } + } + + // Diese Prüfung wird ausgeführt, wenn Einschlussfilter vorhanden sind + if (include_count > 0) { + if (filters.combination_mode == 0) { // AND-Modus + return include_matches == include_count; // Alle Einschlussfilter müssen zutreffen, die Treffer müssen Anzahl der Filter entsprechen + } else { // OR-Modus + return include_matches > 0; // Ausschlussfilter im ODER-Modus - wenn ein beliebiger Eintrag zum Ausschlussfilter passt, wird 0=negativ zurückgegeben + } + } + + return 1; // Keine Einschlussfilter, keine zutreffenden Ausschlussfilter, positiver Rückgabewert =1 +} + +// 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) { + if (filters.url_count == 0) return 1; + // Ausschluss-Filter geht vor + for (int i = 0; i < filters.url_count; i++) { + if (filters.url_filters[i].filter_exclude_flag == 1) { + int pattern_found = search_in_string(url_path, filters.url_filters[i].pattern); + if (pattern_found) { + return 0; // früheres Verlassen der Schleife, sobald Ausschlussfilter zutrifft + } + } + } + // Prüfung im entsprechenden Modus + int include_count = 0; + int include_matches = 0; + + for (int i = 0; i < filters.url_count; i++) { + if (filters.url_filters[i].filter_exclude_flag == 0) { + include_count++; + int pattern_found = search_in_string(url_path, filters.url_filters[i].pattern); + if (pattern_found) { + include_matches++; + if (filters.combination_mode == 1) { // OR-Modus + return 1; // Früheres Verlassen der Schleife bei erstem zutreffendem Einschlussfilter im OR-Modus + } + } + } + } + + // Diese Prüfung wird ausgeführt, wenn Einschlussfilter vorhanden sind + if (include_count > 0) { + if (filters.combination_mode == 0) { // AND-Modus + return include_matches == include_count; // Alle Einschlussfilter müssen zutreffen, die Treffer müssen Anzahl der Filter entsprechen + } else { // OR-Modus + return include_matches > 0; // Ausschlussfilter im ODER-Modus - wenn ein beliebiger Eintrag zum Ausschlussfilter passt, wird 0=negativ zurückgegeben + } + } + + return 1; // Keine Einschlussfilter, keine zutreffenden Ausschlussfilter, positiver Rückgabewert =1 +} + +// 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) { + if (filters.method_count == 0) return 1; + // Ausschluss-Filter geht vor + for (int i = 0; i < filters.method_count; i++) { + if (filters.method_filters[i].filter_exclude_flag == 1) { + int pattern_found = search_in_string(request_method, filters.method_filters[i].pattern); + if (pattern_found) { + return 0; // früheres Verlassen der Schleife, sobald Ausschlussfilter zutrifft + } + } + } + // Prüfung im entsprechenden Modus + int include_count = 0; + int include_matches = 0; + + for (int i = 0; i < filters.method_count; i++) { + if (filters.method_filters[i].filter_exclude_flag == 0) { + include_count++; + int pattern_found = search_in_string(request_method, filters.method_filters[i].pattern); + if (pattern_found) { + include_matches++; + if (filters.combination_mode == 1) { // OR-Modus + return 1; // Früheres Verlassen der Schleife bei erstem zutreffendem Einschlussfilter im OR-Modus + } + } + } + } + + // Diese Prüfung wird ausgeführt, wenn Einschlussfilter vorhanden sind + if (include_count > 0) { + if (filters.combination_mode == 0) { // AND-Modus + return include_matches == include_count; // Alle Einschlussfilter müssen zutreffen, die Treffer müssen Anzahl der Filter entsprechen + } else { // OR-Modus + return include_matches > 0; + } + } + + return 1; // Keine Einschlussfilter, keine zutreffenden Ausschlussfilter, positiver Rückgabewert =1 +} + +int status_code_matches(int status_code) { + if (filters.status_count == 0) return 1; + // Ausschluss-Filter prüfen: immer übergeordnet gültig + for (int i = 0; i < filters.status_count; i++) { + if (filters.status_filters[i].filter_exclude_flag == 1) { + if (filters.status_filters[i].code == status_code) { + return 0; + } + } + } + + // Filter prüfen in entsprechendem Modus + int include_count = 0; + int include_matches = 0; + + for (int i = 0; i < filters.status_count; i++) { + if (filters.status_filters[i].filter_exclude_flag == 0) { + include_count++; + if (filters.status_filters[i].code == status_code) { + include_matches++; + if (filters.combination_mode == 1) { // OR-Modus + return 1; // positiver Rückgabewert, Schleife wird verlassen sobald erster Treffer im OR-Modus + } + } + } + } + + // Diese Prüfung wird ausgeführt, wenn Einschlussfilter vorhanden sind + if (include_count > 0) { + if (filters.combination_mode == 0) { // UND-Modus + return include_matches == include_count; // Filter-Treffer müssen der Anzahl der Statuscode-Filter entsprechen + } else { // OR-Modus + return include_matches > 0; // Ausschlussfilter im ODER-Modus - wenn ein beliebiger Eintrag zum Ausschlussfilter passt, wird 0=negativ zurückgegeben + } + } + return 1; // Keine Einschlussfilter, keine Treffer in den Ausschlussfiltern, positiver Rückgabewert = 1 +} + +int ip_address_matches(char* ip_address) { + if (filters.ip_count == 0) return 1; + + // Prüfen der Ausschlussfilter, sind dem Rest vorgelagert + for (int i = 0; i < filters.ip_count; i++) { + if (filters.ip_filters[i].filter_exclude_flag == 1) { + if (strcmp(filters.ip_filters[i].ip_address, ip_address) == 0) { + return 0; // zutreffender Ausschlussfilter führt zu negativem Rückgabewert + } + } + } + + // Prüfung im AND oder im OR-Modus + int include_count = 0; + int include_matches = 0; + + for (int i = 0; i < filters.ip_count; i++) { + if (filters.ip_filters[i].filter_exclude_flag == 0) { + include_count++; + if (strcmp(filters.ip_filters[i].ip_address, ip_address) == 0) { + include_matches++; + if (filters.combination_mode == 1) { // OR-Modus + return 1; // Früheres Verlassen der Schleife, sofern erster Filter im OR-Modus zutrifft + } + } + } + } + + // Diese Prüfung wird ausgeführt, wenn Einschlussfilter vorhanden sind + if (include_count > 0) { + if (filters.combination_mode == 0) { // UND-Modus + return include_matches == include_count; // Filter-Treffer müssen der Anzahl der IP-Adressen-Filter entsprechen + } else { // OR-Modus + return include_matches > 0; // zutreffender Ausschlussfilter führt zu negativem Rückgabewert + } + } + return 1; // Keine Einschlussfilter, keine Treffer in den Ausschlussfiltern, positiver Rückgabewert = 1 +} + +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) { + if (filters.annotation_count == 0) return 1; + // Ausschluss-Filter geht vor + for (int i = 0; i < filters.annotation_count; i++) { + if (filters.annotation_filters[i].filter_exclude_flag == 1) { + int pattern_found = search_in_string(annotation, filters.annotation_filters[i].pattern); + if (pattern_found) { + return 0; // früheres Verlassen der Schleife, sobald Ausschlussfilter zutrifft + } + } + } + // Prüfung im entsprechenden Modus + int include_count = 0; + int include_matches = 0; + + for (int i = 0; i < filters.annotation_count; i++) { + if (filters.annotation_filters[i].filter_exclude_flag == 0) { + include_count++; + int pattern_found = search_in_string(annotation, filters.annotation_filters[i].pattern); + if (pattern_found) { + include_matches++; + if (filters.combination_mode == 1) { // OR-Modus + return 1; // Früheres Verlassen der Schleife bei erstem zutreffendem Einschlussfilter im OR-Modus + } + } + } + } + + // Diese Prüfung wird ausgeführt, wenn Einschlussfilter vorhanden sind + if (include_count > 0) { + if (filters.combination_mode == 0) { // AND-Modus + return include_matches == include_count; // Alle Einschlussfilter müssen zutreffen, die Treffer müssen Anzahl der Filter entsprechen + } else { // OR-Modus + return 0; + } + } + + return 1; // Keine Einschlussfilter, keine zutreffenden Ausschlussfilter, positiver Rückgabewert =1 +} + +// 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 entry_time) { + 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 + } + } + } + + // Prüfung im entsprechenden Modus + int include_count = 0; + int include_matches = 0; + for (int i = 0; i < filters.time_count; i++) { + if (filters.time_filters[i].filter_exclude_flag == 0) { + include_count++; + 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) { + include_matches++; + if (filters.combination_mode == 1) { // OR-Modus + return 1; // Sobald der erste Zeitraum im OR-Modus zutrifft, wird die Schleife verlassen + } + } + } + } + + // Diese Prüfung wird ausgeführt, wenn Einschlussfilter vorhanden sind + if (include_count > 0) { + if (filters.combination_mode == 0) { // AND-Modus + return include_matches == include_count; // Filter-Treffer müssen der Anzahl der Zeitraum-Filter entsprechen + } else { // OR-Modus + return include_matches > 0; // zutreffender Ausschlussfilter führt zu negativem Rückgabewert + } + } + return 1; // keine Einschlussfilter und keine zutreffenden Ausschlussfilter - positiver Rückgabewert +} + +// Prüfen aller Filter im AND-Modus oder OR-Modus (combination_mode) pro Log-Eintrag +int passes_filter(int entry_index) { + if (filters.combination_mode == 0) { // AND-Modus + // alle AND-verknüpften Filter müssen zutreffen + 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; + } else { // OR-Modus + // für den geprüften Eintrag muss mindestens eine der Filterkategorien zutreffen. + // Eine Prüfung über Filter1 || Filter2 || etc ist nicht möglich, da auch + // nicht gesetzte Filter hier keine Einschränkng bilden. Daher muss jede KAtegorie einzeln geprüft werden. + 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 1; + + int has_passing_filter = 0; + + // Jede Filterkategorie wird nur geprüft, wenn auch entsprechende Filter gesetzt sind. + if (filters.status_count > 0) { + if (status_code_matches(all_entries[entry_index].status_code)) { + has_passing_filter = 1; + } + } + if (filters.method_count > 0) { + if (method_matches(all_entries[entry_index].request_method)) { + has_passing_filter = 1; + } + } + if (filters.ip_count > 0) { + if (ip_address_matches(all_entries[entry_index].ip_address)) { + has_passing_filter = 1; + } + } + if (filters.time_count > 0) { + if (time_matches(all_entries[entry_index].time)) { + has_passing_filter = 1; + } + } + if (filters.user_agent_count > 0) { + if (user_agent_matches(all_entries[entry_index].user_agent)) { + has_passing_filter = 1; + } + } + if (filters.url_count > 0) { + if (url_matches(all_entries[entry_index].url_path)) { + has_passing_filter = 1; + } + } + if (filters.annotation_flag_filter_enabled){ + if (is_annotated(all_entries[entry_index].annotated_flag)) { + has_passing_filter = 1; + } + } + if (filters.annotation_count > 0){ + if (annotation_matches(all_entries[entry_index].annotation)){ + has_passing_filter = 1; + } + } + return has_passing_filter; + } +} + +// 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 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 + fprintf(file, "datetime,message,timestamp_desc,ip_address,method,url_path,status_code,bytes_sent,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].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 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) { + strcpy(ip_stats[unique_ips].ip_address, current_ip); + 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 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 { + char user_agent[256]; + int count; + }; + + struct user_agent_stat 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) { + strcpy(agent_stats[unique_agents].user_agent, current_agent); + 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 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(" "); + } + + if (filters.combination_mode == 1) { + printf("--mode=or "); + }else { + printf("--mode=and "); + } + + 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); + //nicht verifiziert? + printf(" Speicherbelegung: %lu Bytes\n", (unsigned long)(max_entries * sizeof(struct log_entry))); + } 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"); + + int active_types = (filters.status_count > 0) + (filters.method_count > 0) + (filters.ip_count > 0) + (filters.user_agent_count > 0) + (filters.time_count > 0) + (filters.url_count > 0) + (filters.annotation_flag_filter_enabled > 0) + (filters.annotation_count > 0); + if (active_types > 1) { + printf(" %s-Verknüpfung (nur Einschlussfilter, Ausschlussfilter sind stets ODER-verknüpft)\n", filters.combination_mode == 0 ? "UND" : "ODER"); + } + } + + 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"); + printf(" --mode=and UND-Verknüpfung (Standard)\n"); + printf(" --mode=or ODER-Verknüpfung\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. Bei mehreren Filtern verschiedener Typen: Kombination nach --mode\n"); + printf(" 3. Bei mehreren Filtern gleichen Typs: Kombination nach --mode\n\n"); + + printf("PRAKTISCHE BEISPIELE:\n\n"); + + printf("SICHERHEITSANALYSE:\n"); + printf("Verdächtige Pfad-Zugriffe finden:\n"); + printf(" --url=.git,.env,wp-admin,phpmyadmin --mode=or\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 --mode=or\n"); + printf(" > Kombiniert automatische Erkennung mit manuellen Mustern\n\n"); + + printf("Bot-Traffic ausschließen:\n"); + printf(" --useragent=!bot,!crawler,!spider --status=200 --mode=and\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 --mode=and\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 --mode=and\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 --mode=and\n"); + printf(" > Detaillierte Analyse eines Sicherheitsvorfalls\n\n"); + + printf("Fehlgeschlagene Login-Versuche:\n"); + printf(" --url=login,signin,auth --status=401,403 --annotation=Failed --mode=and\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 --mode=and\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 --mode=and\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 --mode=or\n"); + printf(" > Überlastungsindikationen und Rate-Limiting\n\n"); + + printf("Webshell-Suche:\n"); + printf(" --url=.php,cmd=,exec= --annotation=Shell --method=POST,GET --mode=or\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 --mode=and\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; + + strcpy(filters.ip_filters[filters.ip_count].ip_address, ip); + 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 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. 2023): ", 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; + + strcpy(filters.user_agent_filters[filters.user_agent_count].pattern, pattern); + 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; + + strcpy(filters.method_filters[filters.method_count].pattern, pattern); + 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; + + strcpy(filters.url_filters[filters.url_count].pattern, pattern); + 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; + } else if (choice == 8) { + if (filters.annotation_count >= MAX_FILTERS) { + printf("ERROR: Maximale Anzahl Annotations-Filter erreicht (%d)!\n", MAX_FILTERS); + continue; + } + + char pattern[MAX_REQUEST_LENGTH]; + int result = safe_read_string("Annotations-Suchtext eingeben (z.B. 'long request'): ", 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; + + strcpy(filters.annotation_filters[filters.annotation_count].pattern, pattern); + 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-6 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", 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-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_filter_mode(){ + printf("\nFILTER-MODUS ÄNDERN\n"); + printf("Aktueller Modus: %s\n\n", filters.combination_mode == 0 ? "AND (alle müssen zutreffen)" : "OR (einer muss zutreffen)"); + + printf("HINWEIS: Der Modus gilt nur für Einschluss-Filter der gleichen Kategorie.\n"); + printf("Ausschluss-Filter arbeiten immer im OR-Modus und haben Vorrang.\n\n"); + + printf("1. AND-Modus (alle Einschluss-Filter müssen zutreffen)\n"); + printf("2. OR-Modus (mindestens ein Einschluss-Filter muss zutreffen)\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.combination_mode = 0; + printf("Filter-Modus auf AND gesetzt.\n"); + } else if (choice == 2) { + filters.combination_mode = 1; + printf("Filter-Modus auf OR gesetzt.\n"); + } else if (choice == -2 || choice == -3) { + return; + } else if (choice != -1) { + printf("ERROR: Ungültige Auswahl! Bitte wählen Sie 1-3 oder b/m/q.\n"); + } +} + +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; + + 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.combination_mode = 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. Filter-Modus ändern (AND/OR)\n"); + printf("4. Alle Filter zurücksetzen\n"); + printf("5. 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_filter_mode(); + } else if (choice == 4) { + menu_reset_filters(); + } else if (choice == 5) { + 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-5 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 (temporär)\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(0); + } 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-4 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", value); + } + 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 + strcpy(filters.ip_filters[filters.ip_count].ip_address, value); + 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; + } + + strcpy(filters.method_filters[filters.method_count].pattern, value); + 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; + } + + strcpy(filters.user_agent_filters[filters.user_agent_count].pattern, value); + 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; + } + + strcpy(filters.url_filters[filters.url_count].pattern, value); + 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 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\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; + } + + strcpy(filters.annotation_filters[filters.annotation_count].pattern, value); + 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; + } + + //int filterstr_start = arg -2; + // 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 if (strstr(filter_type, "mode") != NULL) { + if (strstr(values, "and") != NULL || strstr(values, "AND") != NULL) { + filters.combination_mode = 0; + if (flag_verbose) printf("DEBUG: AND-Modus gesetzt\n"); + } else if (strstr(values, "or") != NULL || strstr(values, "OR") != NULL) { + filters.combination_mode = 1; + if (flag_verbose) printf("DEBUG: OR-Modus gesetzt\n"); + } else { + printf("WARNING: ungültiger Modus-Wert: %s ('and' oder 'oder' möglich)\n", values); + } + } else { + printf("WARNING: Unbekannter Filtertyp: %s\n", filter_type); + return 0; + } + + return 1; +} + + +// DISCLAIMER: KI-generiert +void print_help(char* binary) { + printf("\nNGINX EXAMINATOR\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(" -h Zeigt diese Hilfe an.\n"); + + printf("\nKOMMNDOZEILEN-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(" --mode=MODE Filtermodus: 'and' oder 'or' (Standard: and)\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:\n"); + printf(" - Ausschluss-Filter (!) haben IMMER Vorrang vor Einschluss-Filtern\n"); + printf(" - AND-Modus: ALLE Einschluss-Filter pro Kategorie müssen zutreffen\n"); + printf(" - OR-Modus: MINDESTENS EIN Einschluss-Filter pro Kategorie muss zutreffen\n"); + printf(" - Ausschluss-Filter arbeiten kategorie-intern immer im OR-Modus\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:\n"); + printf(" - Automatische Erkennung verdächtiger Muster (z.B. 'Long Payload')\n"); + printf(" - Filterbar über --annotated und --annotation Parameter\n"); + printf(" - Sichtbar in interaktivem Modus und CSV-Export\n"); + + printf("\nHinweise & Einschränkungen:\n"); + printf(" - Parser erwartet das obige Standardformat. Abweichungen können zum Abbruch führen.\n"); + printf(" - Zeitzone aus dem Log wird gelesen, aber intern als einfache Felder verarbeitet.\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\n"); + + // Dateipfad: wenn nicht angegeben, wird /var/log/nginx (Standardpfad) untersucht + char* input_path; + int arg_offset = 1; // Offset für die args + + // Prüfen des ersten Zeichens des ersten Arguments - Pfad oder Flag? + if (argv[1][0] == '-') { + // Ist ein Flag - Standardpfad, Offset bleibt bei 1 + input_path = "/var/log/nginx/"; + arg_offset = 1; + } else { + input_path = argv[1]; + arg_offset = 2; // Offset inkrementieren, Dateipfad schiebt die args nach hinten + } + + + int flag_interactive = 0; + int flag_export = 0; + int flag_help = 0; + // int flag_verbose = 0; - global definiert + + char export_filename[90]; + int flag_has_filename = 0; + + allocate_initial_memory(); + + if (argc >= 2){ + // hier wird das offset angewendet + for (int i=arg_offset; i +#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. + +// 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 { + int day; + int month; + int year; + int hour; + int minute; + int second; +}; + +// Struktur für die Darstellung eines Standard-NGINX-Logeintrags. +struct log_entry { + 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 time; + char referrer[128]; + char user_agent[256]; + char source_file[256]; + char parsing_timestamp[32]; +}; + +// Struktur für einen Status-Filtereintrag mit Inhalt & Modus +struct status_filter { + int code; + int filter_exclude_flag; +}; + +struct method_filter { + char pattern[10]; + int filter_exclude_flag; +}; + +// für IP-Adressen +struct ip_filter { + char ip_address[50]; + int filter_exclude_flag; +}; + +// Filter für User-Agent +struct user_agent_filter { + char pattern[256]; + int filter_exclude_flag; +}; + +// Filter für URL-Pfad/Request +struct url_filter { + char pattern[MAX_REQUEST_LENGTH]; + int filter_exclude_flag; +}; + +// Struktur zum erhalten aller Filtereinträge, kann im Dialogbetrieb bearbeitet werden. Mit Zähler. +struct filter_system { + struct status_filter status_filters[MAX_FILTERS]; + int status_count; + + struct method_filter method_filters[MAX_FILTERS]; + int method_count; + + struct ip_filter ip_filters[MAX_FILTERS]; + int ip_count; + + struct user_agent_filter user_agent_filters[MAX_FILTERS]; + int user_agent_count; + + struct url_filter url_filters[MAX_FILTERS]; + int url_count; +}; + +// Initialisierung eines Arrays für die Logeinträge und weiterer Startvariablen +struct log_entry *all_entries = NULL; +int max_entries = 0; +int total_entries = 0; +struct filter_system 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; +} + +// 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))); + free(all_entries); + all_entries = NULL; + } + max_entries = 0; + total_entries = 0; +} + +// Standardfunktion zum Leeren des Input Buffers +void clear_input_buffer(){ + int c; + while ((c = getchar()) != '\n' && c != EOF) { + } +} + +// 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 *new_ptr = realloc(all_entries, max_entries * sizeof(struct log_entry)); + + 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))); + 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))); + } +} + +void allocate_initial_memory(){ + max_entries = INITIAL_ENTRIES; // Startwert 1000, globale Variable + all_entries = malloc(max_entries * sizeof(struct log_entry)); + + 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))); + 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))); +} + +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; + } +} + +/* +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 + 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 + strcpy(all_entries[entry_index].request_method, temp_method); + 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 + strcpy(all_entries[entry_index].request_method, "ATYPICAL"); + + // 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 "", wird übersprungen da nicht gespeichert + if (*current_pos == '"') { + current_pos++; // öffnendes Anführungszeichen überspringen + // Referrer-Inhalt bis zum schließenden Anführungszeichen überspringen + while (*current_pos != '"' && *current_pos != '\0') { + current_pos++; + } + 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); + if (flag_verbose) printf("DEBUG: Aktueller Speicherverbrauch: %lu Bytes für %d Einträge\n", + (unsigned long)(max_entries * sizeof(struct log_entry)), max_entries); +} + +// 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; +} + +// Prüfen aller Filter, alle müssen zutreffen +int passes_filter(int entry_index) { + // AND-Verknüpfung + 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 user_agent_match = user_agent_matches(all_entries[entry_index].user_agent); + int url_match = url_matches(all_entries[entry_index].url_path); + + return status_match && ip_match && user_agent_match && method_match && url_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 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); +} + +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"); + 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 + fprintf(file, "datetime,message,timestamp_desc,ip_address,method,url_path,status_code,bytes_sent,user_agent,parsing_timestamp\n"); + + 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)); + fprintf(file, "%s,%s,\"NGINX Log\",%s,%s,%s,%d,%d,%s,%s\n", iso_datetime, all_entries[i].source_file, 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].parsing_timestamp); + } + } + + fclose(file); + printf("INFO: Logeinträge erfolgreich als Timesketch-kompatible CSV-Datei nach '%s' exportiert.\n", filename); +} + +// Funktionen zum setzen der Filter (existierende Datenstrukturen) +void apply_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", value); + } + 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 apply_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 + strcpy(filters.ip_filters[filters.ip_count].ip_address, value); + 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 apply_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; + } + + strcpy(filters.method_filters[filters.method_count].pattern, value); + 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 apply_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; + } + + strcpy(filters.user_agent_filters[filters.user_agent_count].pattern, value); + 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 apply_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; + } + + strcpy(filters.url_filters[filters.url_count].pattern, value); + 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); +} + +// 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) { + apply_status_filter(token, filter_exclude_flag); + } else if (strcmp(filter_type, "ip") == 0) { + apply_ip_filter(token, filter_exclude_flag); + } else if (strcmp(filter_type, "method") == 0) { + apply_method_filter(token, filter_exclude_flag); + } else if (strcmp(filter_type, "useragent") == 0) { + apply_useragent_filter(token, filter_exclude_flag); + } else if (strcmp(filter_type, "url") == 0) { + apply_url_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; + } + + //int filterstr_start = arg -2; + // filter-Typ parsen: 1. Länge des Strings errechnen (Position des '=' abzüglich Startposition des Strings (arg), 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 { + printf("WARNING: Unbekannter Filtertyp: %s\n", filter_type); + return 0; + } + + return 1; +} + +void print_help(char* binary) { + printf("NGINX-Auditor (Beleg)\n"); + printf("Nutzung: %s [Flags]\n\n", binary); + printf("Flags:\n"); + printf(" -e [Dateiname ohne Endung] Export zu Timestamp-kompatiblem CSV\n"); + printf(" -f --[Filterobjekt] Filtern mit den folgenden Optionen:\n"); + printf(" --status=[HTTP-Statuscode],[Weiterer],[...] HTTP Status Codes, z.B. 200, 404,301,...\n"); + printf(" --ip=[ip1],[ip2],[ip3] IP-Adressen\n"); + printf(" --method=[methode1],[...] HTTP-Methoden (z.B. GET,POST,...)\n"); + printf(" --useragent=[Freitext1],[...] Text im User-Agent, z.B. bot, crawler, uptime-kuma\n"); + printf(" --url=[Freitext1],[...] Text im URL-Pfad/Payload\n"); + printf(" -v Mehr Informationen auf stdout ausgeben\n"); + printf(" -h Diese Hilfe anzeigen\n"); + printf("Filter Syntaxhinweis: !Wert nutzen, um einen exklusiven Filter zu nutzen.\n"); + printf("Datei-Parsing: Unterstützt NGINX Accesslogs und Redirect-Accesslogs, auch .gz komprimierte rotierte Logs.\n"); +} + +int main(int argc, char* argv[]) { + if (argc < 2) { + print_help(argv[0]); + return 1; + } + + printf("\nNGINX EXAMINATOR\n"); + + // Dateipfad: wenn nicht angegeben, wird /var/log/nginx (Standardpfad) untersucht + char* input_path; + int arg_offset = 1; // Offset für die args + + // Prüfen des ersten Zeichens des ersten Arguments - Pfad oder Flag? + if (argv[1][0] == '-') { + // Ist ein Flag - Standardpfad, Offset bleibt bei 1 + input_path = "/var/log/nginx/"; + arg_offset = 1; + } else { + input_path = argv[1]; + arg_offset = 2; // Offset inkrementieren, Dateipfad schiebt die args nach hinten + } + int flag_export = 0; + int flag_help = 0; + // int flag_verbose = 0; - global definiert + + char export_filename[90]; + int flag_has_filename = 0; + + allocate_initial_memory(); + + if (argc >= 2){ + // hier wird das offset angewendet + for (int i=arg_offset; i