This commit is contained in:
overcuriousity 2025-09-04 16:13:30 +02:00
parent 654641c87a
commit a06777e754
3 changed files with 665 additions and 74 deletions

242
lib/gzunpack.c Normal file
View File

@ -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 <stdlib.h>
#include <string.h>
#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;
}

56
lib/gzunpack.h Normal file
View File

@ -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 <stdio.h>
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

View File

@ -12,21 +12,28 @@ Redistribution and use in source and binary forms, with or without modification,
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.
*/
// KOMPILIERUNG:
// gcc -o bin/main src/main.c lib/gzunpack.c -Ilib -lz
// ABHÄNGIGKEITEN:
// gcc, zlib-devel
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <dirent.h> // library für Interaktion mit Ordnerstrukturen
#include <sys/stat.h> // library für is_directory: Unterscheidung zwischen Dateien und Ordnern
#include <time.h> // um aktuelle Zeit zu generieren
#include "gzunpack.h"
#define MAX_LINE_LENGTH_BUF 2048
#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 1024 // das hohe Limit ist erforderlich, da teilweise ausufernde JSON-Requests in nginx auflaufen können.
#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
// definiert Variablen für den Filtermodus. FILTER_INCLUDE=0, FILTER_EXCLUDE=1. Verbessert die Lesbarkeit des Codes.
// TODO entfernen?
typedef enum {
FILTER_INCLUDE,
FILTER_EXCLUDE
@ -46,7 +53,7 @@ struct simple_time {
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 modernen Applikationen oder Malware verwendet
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;
@ -54,6 +61,8 @@ struct log_entry {
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
@ -92,6 +101,16 @@ struct url_filter {
filter_mode_t mode;
};
struct annotation_flag_filter {
int annotation_flag_is_set;
filter_mode_t mode;
};
struct annotation_filter {
char pattern[64];
filter_mode_t mode;
};
// Struktur zum erhalten aller Filtereinträge, kann im Dialogbetrieb bearbeitet werden. Mit Zähler.
struct filter_system {
struct status_filter status_filters[MAX_FILTERS];
@ -111,6 +130,12 @@ struct filter_system {
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
};
@ -119,6 +144,7 @@ struct filter_system {
struct log_entry *all_entries = NULL;
int max_entries = 0;
int total_entries = 0;
int suspicious_patterns_count = 0;
struct filter_system filters = {0};
// Hilfsfunktion für die Erkennung von Leerzeichen
@ -187,14 +213,14 @@ int compare_times(struct simple_time time_checkvalue, struct simple_time time_fi
}
// Standardfunktion zum Leeren des Input-Buffers
void clear_input_buffer() {
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 read_safe_integer(){
int number;
int result = scanf("%d", &number);
@ -208,7 +234,7 @@ int read_safe_integer() {
}
// Speicher freigeben und mit 0 überschreiben (Prävention von use-after-free-Schwachstelle)
void cleanup_memory() {
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);
@ -219,14 +245,14 @@ void cleanup_memory() {
}
// sauberes Schließen und bereinigen bei Fehlerstatus, sofern Speicher nicht alloziert werden kann
void cleanup_and_exit() {
void cleanup_and_exit(){
printf("Programmende. Speicher wird freigegeben und mit NULL überschrieben.\n");
cleanup_memory();
exit(1);
}
// Erweiterung des Speichers für dynamische Speicherallokation
void mem_expand_dynamically() {
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;
@ -247,7 +273,7 @@ void mem_expand_dynamically() {
}
}
void allocate_initial_memory() {
void allocate_initial_memory(){
max_entries = INITIAL_ENTRIES; // Startwert 1000, globale Variable
all_entries = malloc(max_entries * sizeof(struct log_entry));
@ -305,6 +331,35 @@ int is_log_file(char* filename) {
return 0; // false, wenn nichts zutrifft
}
// Fügt eine Annotation zu einem bestimmten Eintrag hinzu
void annotate_entry(int index, char* annotation_string) {
if (index >= 0 && index < total_entries) {
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;
//printf("DEBUG: Annotation zu Eintrag %d: %s\n", index, annotation_string);
}
}
void annotate_suspicious_entries(struct log_entry* dataset) {
printf("DEBUG: Prüfe %d Einträge auf verdächtige Muster...\n", total_entries);
for (int i=0; i< total_entries; i++){
if (all_entries[i].annotation[0] == '\0') {
all_entries[i].annotation[0] = '\0';
}
// Prüfmuster: Sehr lange Requests
int url_length = strlen(all_entries[i].url_path);
if (url_length>SUSPICIOUS_REQUEST_LEN_THRESHOLD){
//printf("DEBUG: URL: %s\nLänge: %d\nmax_länge: %d\n", all_entries[i].url_path, url_length, SUSPICIOUS_REQUEST_LEN_THRESHOLD);
annotate_entry(i, "Long Payload");
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.
@ -318,6 +373,7 @@ Fehleranfällig, wenn das Logformat nicht dem Standard entspricht - das gilt abe
*/
// 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);
@ -351,8 +407,7 @@ int parse_simple_log_line(char* line, int entry_index, char* source_file) { // N
// ^
// 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');
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++;
}
@ -360,8 +415,7 @@ int parse_simple_log_line(char* line, int entry_index, char* source_file) { // N
char month_str[4] = {0};
int month_pos = 0;
while (*current_pos != '/' && *current_pos != '\0' && month_pos < 3) {
month_str[month_pos] = *current_pos;
while (*current_pos != '/' && *current_pos != '\0' && month_pos < 3) {month_str[month_pos] = *current_pos;
month_pos++;
current_pos++;
}
@ -404,7 +458,7 @@ int parse_simple_log_line(char* line, int entry_index, char* source_file) { // N
// 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]\n\n");
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();
}
@ -437,7 +491,9 @@ int parse_simple_log_line(char* line, int entry_index, char* source_file) { // N
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++;
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);" "-"
// ^
@ -461,8 +517,7 @@ int parse_simple_log_line(char* line, int entry_index, char* source_file) { // N
// 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) {
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++;
@ -479,7 +534,7 @@ int parse_simple_log_line(char* line, int entry_index, char* source_file) { // N
// 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\"\n\n");
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();
}
@ -513,7 +568,7 @@ int parse_simple_log_line(char* line, int entry_index, char* source_file) { // N
}
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.\n\n");
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();
}
@ -530,7 +585,7 @@ int parse_simple_log_line(char* line, int entry_index, char* source_file) { // N
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);\"\n\n");
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));
@ -538,6 +593,7 @@ int parse_simple_log_line(char* line, int entry_index, char* source_file) { // N
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;
}
@ -548,7 +604,7 @@ void load_regular_file(char* filename) {
return;
}
printf("INFO: Lade Datei: %s\n", filename);
char line[MAX_LINE_LENGTH_BUF];
char line[MAX_REQUEST_LENGTH];
int loaded_from_this_file = 0;
while (fgets(line, sizeof(line), file) != NULL) {
mem_expand_dynamically();
@ -561,66 +617,104 @@ void load_regular_file(char* filename) {
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)) {
printf("DEBUG: Verzeichnis erkannt: %s\n", path);
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;
char* filename = (*entry).d_name;
if (strcmp(filename, ".") == 0 || strcmp(filename, "..") == 0) {
printf("WARNING: Überspringe Datei %s, unerwartete Dateisignatur", 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;
}
if (is_log_file(filename)) {
char full_path[512];
snprintf(full_path, sizeof(full_path), "%s/%s", path, filename);
load_regular_file(full_path);
files_found++;
} else if (strstr(filename, ".gz") != NULL) {
printf("INFO: .gz Datei '%s' übersprungen (nicht unterstützt in dieser Version)\n", filename);
printf(" Tipp: Dekomprimieren Sie mit 'gunzip %s'\n", 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 {
printf("Einzelne Datei erkannt: %s\n", path);
printf("DEBUG: Einzelne Datei erkannt: %s\n", path);
if (strstr(path, ".gz") != NULL) {
printf("ERROR: .gz Dateien werden in dieser Version nicht unterstützt!\n");
printf(" Lösung: Dekomprimieren Sie die Datei zuerst:\n");
printf(" gunzip %s\n", path);
printf(" Dann: %s %.*s\n", "PROGRAMM", (int)(strlen(path)-3), path);
return;
load_gz_file(path);
} else {
load_regular_file(path);
}
}
printf("INFO: Erfolgreich %d Einträge insgesamt geladen.\n", total_entries);
printf("DEBUG: Aktueller Speicherverbrauch: %lu Bytes für %d Einträge\n", (unsigned long)(max_entries * sizeof(struct log_entry)), max_entries);
annotate_suspicious_entries(all_entries);
printf("DEBUG: Aktueller Speicherverbrauch: %lu Bytes für %d Einträge\n",
(unsigned long)(max_entries * sizeof(struct log_entry)), max_entries);
}
// Funktion zum suchen eines Suchbegriffs innerhalb eines Strings (lowercase)
@ -864,6 +958,62 @@ int ip_address_matches(char* ip_address) {
return 1; // Keine Einschlussfilter, keine Treffer in den Ausschlussfiltern, positiver Rückgabewert = 1
}
int is_annotated(int annotated_flag){
if (filters.annotation_flag_filter.mode == FILTER_EXCLUDE && 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.mode == FILTER_INCLUDE && 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.mode == FILTER_INCLUDE && 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].mode == FILTER_EXCLUDE) {
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].mode == FILTER_INCLUDE) {
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;
@ -917,14 +1067,15 @@ int passes_filter(int entry_index) {
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;
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;
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;
@ -965,13 +1116,25 @@ int passes_filter(int entry_index) {
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_filtered_entries(){
int count = 0;
for (int i = 0; i < total_entries; i++) {
if (passes_filter(i)) {
@ -1012,7 +1175,7 @@ void export_filtered_entries(char *filepath) {
}
// CSV-Kopfzeile für Timesketch-Kompatibilität
fprintf(file, "datetime,timestamp_desc,ip_address,method,url_path,status_code,bytes_sent,user_agent,source_file,parsing_timestamp\n");
fprintf(file, "datetime,timestamp_desc,ip_address,method,url_path,status_code,bytes_sent,user_agent,source_file,parsing_timestamp,annotation\n");
int exported_count = 0;
char iso_datetime[32];
@ -1032,7 +1195,9 @@ void export_filtered_entries(char *filepath) {
all_entries[i].bytes_sent,
all_entries[i].user_agent,
all_entries[i].source_file,
all_entries[i].parsing_timestamp);
all_entries[i].parsing_timestamp,
all_entries[i].annotation
);
exported_count++;
}
@ -1047,7 +1212,44 @@ struct ip_stat {
int count;
};
void show_top_x_ips() {
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);
}
}
void show_top_x_ips(){
struct ip_stat ip_stats[1000];
int unique_ips = 0;
@ -1101,7 +1303,7 @@ void show_top_x_ips() {
}
}
void show_top_user_agents() {
void show_top_user_agents(){
struct user_agent_stat {
char user_agent[256];
int count;
@ -1164,13 +1366,13 @@ void show_filtered_entries(int num_shown) {
int shown_count = 0;
printf("\nLOGDATEN:\n");
printf("IP-Adresse | Methode | URL | Status | Bytes | User Agent | Zeit\n");
printf("-----------------|---------|------------------------|--------|-------|--------------------------------------|------------------\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\n",
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,
@ -1182,7 +1384,9 @@ void show_filtered_entries(int num_shown) {
all_entries[i].time.year,
all_entries[i].time.hour,
all_entries[i].time.minute,
all_entries[i].time.second);
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
@ -1199,8 +1403,9 @@ void show_filtered_entries(int num_shown) {
}
}
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;
// TODO annotationsfilter, Zeitraumfilter
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;
@ -1257,6 +1462,25 @@ void print_filter_args() {
}
printf(" ");
}
// TODO timestamp
// 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.mode == FILTER_EXCLUDE) 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].mode == FILTER_EXCLUDE) printf("!");
printf("%s", filters.annotation_filters[i].pattern);
}
printf(" ");
}
if (filters.combination_mode == 1) {
printf("--mode=or ");
@ -1272,7 +1496,7 @@ void print_filter_args() {
}
}
void show_status() {
void show_status(){
printf("\nPREVIEW:\n");
show_filtered_entries(10);
printf("\nSTATUS\n");
@ -1285,7 +1509,7 @@ void show_status() {
}
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;
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");
@ -1295,7 +1519,7 @@ void show_status() {
printf("\n <Verwendbar als Startargument>\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);
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");
}
@ -1304,10 +1528,12 @@ void show_status() {
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);
}
}
void print_filter_examples() {
void print_filter_examples(){
printf("\nFILTER-DOKUMENTATION\n");
printf("\nEXKLUSIONS-FILTER (immer OR-Logik, unabhängig vom Modus):\n");
printf("Beispiel: !('uptime' OR 'scanner')\n");
@ -1417,7 +1643,7 @@ int safe_read_string(char* prompt, char* buffer, int buffer_size) {
}
// Menü mit standardisierter Navigation
int read_menu_input() {
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) {
@ -1440,7 +1666,7 @@ int read_menu_input() {
return (int)number;
}
void show_main_menu() {
void show_main_menu(){
printf("\nHAUPTMENÜ\n");
printf("1. Filter verwalten\n");
printf("2. Daten anzeigen und exportieren\n");
@ -1450,7 +1676,7 @@ void show_main_menu() {
printf("Auswahl: ");
}
int menu_set_filters() {
int menu_set_filters(){
int choice = 0;
// Standardnavigation aus read_menu_input
while (choice != -2 && choice != -3) {
@ -1463,6 +1689,8 @@ int menu_set_filters() {
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: ");
@ -1652,7 +1880,36 @@ int menu_set_filters() {
filters.url_filters[filters.url_count].mode = (filter_type == 2) ? FILTER_EXCLUDE : FILTER_INCLUDE;
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.mode = (filter_type == 2) ? FILTER_EXCLUDE : FILTER_INCLUDE;
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].mode = (filter_type == 2) ? FILTER_EXCLUDE : FILTER_INCLUDE;
filters.annotation_count++;
printf("Annotations-Filter hinzugefügt. Gesamt: %d\n", filters.annotation_count);
} else if (choice == -2) {
return -2;
} else if (choice == -3) {
@ -1664,9 +1921,8 @@ int menu_set_filters() {
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;
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");
@ -1721,6 +1977,16 @@ void menu_delete_filters() {
filters.time_filters[i].end_time.second,
mode_str);
}
if (filters.annotation_flag_filter_enabled){
char* mode_str = (filters.annotation_flag_filter.mode == FILTER_EXCLUDE) ? "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].mode == FILTER_EXCLUDE) ? "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;
@ -1802,9 +2068,30 @@ void menu_delete_filters() {
}
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() {
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)");
@ -1835,7 +2122,7 @@ void menu_filter_mode() {
}
}
void menu_reset_filters() {
void menu_reset_filters(){
printf("\nFILTER ZURÜCKSETZEN\n");
int total_filters = filters.status_count + filters.method_count + filters.ip_count +
@ -1863,6 +2150,8 @@ void menu_reset_filters() {
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) {
@ -1874,7 +2163,7 @@ void menu_reset_filters() {
}
}
void menu_filter_management() {
void menu_filter_management(){
int choice = 0;
// Standardnavigation aus read_menu_input
while (choice != -2 && choice != -3) {
@ -1913,7 +2202,7 @@ void menu_filter_management() {
}
}
void menu_show_entries() {
void menu_show_entries(){
int choice = 0;
int supress_preview = 0;
// Standardnavigation aus read_menu_input()
@ -1932,13 +2221,14 @@ void menu_show_entries() {
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 (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");
@ -1959,6 +2249,9 @@ void menu_show_entries() {
show_top_x_ips();
} else if (choice == 4) {
show_top_user_agents();
} else if (choice == 5){
show_annotated_entries();
supress_preview =1;
} else if (choice == -2) {
return;
} else if (choice == -3) {
@ -1978,7 +2271,7 @@ void add_status_filter(char* value, filter_mode_t mode) {
// Kovertierung des Statuscodes zu long mit Error handling
char* endptr;
int status_code = strtol(value, &endptr, 10);
if (*endptr != '\n' ){
if (*endptr != '\n'){
printf("ERROR: Ungültiger Wert im Statuscode-Filter: %s", value);
}
if (status_code < 100 || status_code > 599) {