src/main.c aktualisiert

This commit is contained in:
Mario Stöckl 2025-09-07 17:49:30 +00:00
parent cd57025123
commit 2df49d7860

View File

@ -30,9 +30,10 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
#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 MAX_PREVIEW 10000
// 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 {
struct simple_time_t {
int day;
int month;
int year;
@ -42,13 +43,13 @@ struct simple_time {
};
// Struktur für die Darstellung eines Standard-NGINX-Logeintrags.
struct log_entry {
struct log_entry_t {
char ip_address[50]; // ausreichende Längenbegrenzung für IP-Adressen. Könnte theoretisch auch ipv6 (ungetestet)
char request_method[10]; // GET, POST, PUT, DELETE, PROPFIND ...
char url_path[MAX_REQUEST_LENGTH]; // Pfade können lang werden, insbesodere bei base64-Strings wie oft in Malware verwendet
int status_code;
int bytes_sent;
struct simple_time time;
struct simple_time_t time;
char referrer[128];
char user_agent[256];
char source_file[256];
@ -56,57 +57,57 @@ struct log_entry {
};
// Struktur für einen Status-Filtereintrag mit Inhalt & Modus
struct status_filter {
struct status_filter_t {
int code;
int filter_exclude_flag;
};
struct method_filter {
struct method_filter_t {
char pattern[10];
int filter_exclude_flag;
};
// für IP-Adressen
struct ip_filter {
struct ip_filter_t {
char ip_address[50];
int filter_exclude_flag;
};
// Filter für User-Agent
struct user_agent_filter {
struct user_agent_filter_t {
char pattern[256];
int filter_exclude_flag;
};
// Filter für URL-Pfad/Request
struct url_filter {
struct url_filter_t {
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];
struct filter_system_t {
struct status_filter_t status_filters[MAX_FILTERS];
int status_count;
struct method_filter method_filters[MAX_FILTERS];
struct method_filter_t method_filters[MAX_FILTERS];
int method_count;
struct ip_filter ip_filters[MAX_FILTERS];
struct ip_filter_t ip_filters[MAX_FILTERS];
int ip_count;
struct user_agent_filter user_agent_filters[MAX_FILTERS];
struct user_agent_filter_t user_agent_filters[MAX_FILTERS];
int user_agent_count;
struct url_filter url_filters[MAX_FILTERS];
struct url_filter_t url_filters[MAX_FILTERS];
int url_count;
};
// Initialisierung eines Arrays für die Logeinträge und weiterer Startvariablen
struct log_entry *all_entries = NULL;
struct log_entry_t *all_entries = NULL;
int max_entries = 0;
int total_entries = 0;
struct filter_system filters = {0};
struct filter_system_t filters = {0};
// für -v option
int flag_verbose = 0;
@ -160,7 +161,7 @@ int month_name_to_number(char* month_name) {
// 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)));
printf("\nDEBUG: %lu Bytes Speicher werden freigegeben\n", (unsigned long)(max_entries * sizeof(struct log_entry_t)));
free(all_entries);
all_entries = NULL;
}
@ -191,47 +192,49 @@ void mem_expand_dynamically(){
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));
struct log_entry_t *new_ptr = realloc(all_entries, max_entries * sizeof(struct log_entry_t));
if (new_ptr == NULL){
printf("ERROR: Speicher konnte nicht auf %d Einträge erweitert werden, ..\n", max_entries);
printf("ERROR: Benötigter Speicher: %lu Bytes\n", (unsigned long)(max_entries * sizeof(struct log_entry)));
printf("ERROR: Benötigter Speicher: %lu Bytes\n", (unsigned long)(max_entries * sizeof(struct log_entry_t)));
cleanup_and_exit();
}
all_entries = new_ptr;
if (flag_verbose) printf("DEBUG: Speicher erfolgreich erweitert auf %lu Bytes\n", (unsigned long)(max_entries * sizeof(struct log_entry)));
if (flag_verbose) printf("DEBUG: Speicher erfolgreich erweitert auf %lu Bytes\n", (unsigned long)(max_entries * sizeof(struct log_entry_t)));
}
}
void allocate_initial_memory(){
max_entries = INITIAL_ENTRIES; // Startwert 1000, globale Variable
all_entries = malloc(max_entries * sizeof(struct log_entry));
all_entries = malloc(max_entries * sizeof(struct log_entry_t));
if (all_entries == NULL){
printf("ERROR: Konnte %d Einträge nicht allozieren, ..\n", max_entries);
printf("ERROR: %lu Bytes\n", (unsigned long)(max_entries * sizeof(struct log_entry)));
printf("ERROR: %lu Bytes\n", (unsigned long)(max_entries * sizeof(struct log_entry_t)));
exit(1); // cleanup_and_exit() nicht nötig, da der Speicherbereich nicht beschrieben wurde - use-after-free unproblematisch
}
if (flag_verbose) printf("DEBUG: Speicher erfolgreich alloziert für %d Log-Einträge (%lu Bytes)\n", max_entries, (unsigned long)(max_entries * sizeof(struct log_entry)));
if (flag_verbose) printf("DEBUG: Speicher erfolgreich alloziert für %d Log-Einträge (%lu Bytes)\n", max_entries, (unsigned long)(max_entries * sizeof(struct log_entry_t)));
}
void get_current_timestamp(char* buffer, int buffer_size) {
// aktuelle Timestamp erfassen
void get_current_timestamp(char* buffer){
time_t raw_time;
struct tm *time_info;
int timestamp_buffer_size =32;
time(&raw_time);
time_info = localtime(&raw_time);
if (time_info != NULL){
strftime(buffer, buffer_size, "%Y-%m-%d %H:%M:%S", time_info);
strftime(buffer, timestamp_buffer_size, "%Y-%m-%d %H:%M:%S", time_info);
} else {
snprintf(buffer, buffer_size, "UNKNOWN");
sprintf(buffer, "UNKNOWN");
}
}
// Hilfsfunktion zum Prüfen, ob es sich beim Pfad um ein Directory handelt - für rekursives Parsen
// https://stackoverflow.com/questions/4553012/checking-if-a-file-is-a-directory-or-just-a-file
int is_directory(char* path){
struct stat path_stat;
if (stat(path, &path_stat) != 0){
@ -438,7 +441,7 @@ 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);
strncpy(all_entries[entry_index].request_method, temp_method,sizeof(all_entries[entry_index].request_method));
while (*current_pos != ' ' && *current_pos != '\0'){
current_pos++;
}
@ -461,15 +464,17 @@ int parse_simple_log_line(char* line, int entry_index, char* source_file) { // N
} 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 reicht hier aus, da der Wert "ATYPICAL" hard-gecoded und somit deterministisch ist"
strcpy(all_entries[entry_index].request_method, "ATYPICAL");
// Read entire quoted content into url_path for forensic analysis
// Kompletten Inhalt zwischen "" einlesen
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++;
}
// Nullterminator anfügen
all_entries[entry_index].url_path[i] = '\0';
// zum Ende des request-strings vorarbeiten, wenn der String zu lang war.
@ -507,19 +512,23 @@ int parse_simple_log_line(char* line, int entry_index, char* source_file) { // N
}
current_pos = skip_spaces(current_pos);
// Parsen des Referrer-Feldes innerhalb "", wird übersprungen da nicht gespeichert
// Parsen des Referrer-Feldes innerhalb ""
if (*current_pos == '"'){
current_pos++; // öffnendes Anführungszeichen überspringen
// Referrer-Inhalt bis zum schließenden Anführungszeichen überspringen
while (*current_pos != '"' && *current_pos != '\0') {
// Referrer-Inhalt zwischen "" einlesen
int i = 0;
while (*current_pos != '"' && *current_pos != '\0' && i < sizeof(all_entries[entry_index].referrer) - 1){
all_entries[entry_index].referrer[i] = *current_pos;
i++;
current_pos++;
}
all_entries[entry_index].referrer[i] = '\0';
if (*current_pos == '"') current_pos++; // schließendes Anführungszeichen überspringen
} else {
printf("ERROR: Unerwartetes Log-Format. Lediglich mit standard-nginx-accesslog kompatibel.\nDer Fehler ist beim Prüfen des Referrer-Feldes aufgetreten.\nLogeintrag: %s\n", line);
cleanup_and_exit();
}
current_pos = skip_spaces(current_pos);
// parsen des user agents innerhalb ""
if (*current_pos == '"'){
@ -536,7 +545,7 @@ int parse_simple_log_line(char* line, int entry_index, char* source_file) { // 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));
get_current_timestamp(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
@ -637,11 +646,11 @@ void load_log_file(char* path) {
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);
(needs_slash) ? sprintf(full_path, "%s/%s", path, filename) : sprintf(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);
(needs_slash) ? sprintf(full_path, "%s/%s", path, filename) : sprintf(full_path, "%s%s", path, filename);
load_gz_file(full_path);
files_found++;
}
@ -665,7 +674,7 @@ void load_log_file(char* 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);
(unsigned long)(max_entries * sizeof(struct log_entry_t)), 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
@ -822,10 +831,34 @@ int count_filtered_entries(){
}
// 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 format_datetime(struct simple_time_t time, char* buffer){
sprintf(buffer, "%04d-%02d-%02dT%02d:%02d:%02d+00:00", time.year, time.month, time.day, time.hour, time.minute, time.second);
}
void show_preview(){
char datetime[32];
int lines_shown = 0;
int count_filtered =0;
printf("\n");
printf("| TIMESTAMP | IP-Adresse | HTTP-METHODE | PFAD/PAYLOAD | USERAGENT | STATUSCODE | BYTES | REFERRER |\n");
printf("|---------------------|-----------------|--------------|--------------------------------|----------------------|------------|----------|----------------------|\n");
for (int i = 0; i < MAX_PREVIEW; i++){
if (passes_filter(i)){
format_datetime(all_entries[i].time, datetime);
printf("| %-19.19s | %-15.15s | %-12.12s | %-30.30s | %-20.20s | %-10d | %-8d | %-20.20s |\n", datetime, all_entries[i].ip_address, all_entries[i].request_method, all_entries[i].url_path, all_entries[i].user_agent, all_entries[i].status_code, all_entries[i].bytes_sent, all_entries[i].referrer);
lines_shown++;
}
}
for (int i = 0; i < total_entries; i++){
if (passes_filter(i)){
count_filtered++;
}
}
printf("\n%d Zeilen von insgesamt %d Einträgen(gefiltert) angezeigt.\nInsgesamt %d Einträge im Datensatz.\n", lines_shown, total_entries, count_filtered);
}
void export_filtered_entries(char *filepath){
// 90 chars +delimiter
char filename[91];
@ -836,6 +869,7 @@ void export_filtered_entries(char *filepath) {
return;
}
} else {
// buffer overflow verhindern
strncpy(filename, filepath, sizeof(filename) - 1);
filename[sizeof(filename) - 1] = '\0';
}
@ -851,12 +885,12 @@ void export_filtered_entries(char *filepath) {
// 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];
char 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);
format_datetime(all_entries[i].time, datetime);
fprintf(file, "%s,%s,\"NGINX Log\",%s,%s,%s,%d,%d,%s,%s,%s\n", 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].referrer,all_entries[i].parsing_timestamp);
}
}
@ -902,7 +936,7 @@ void apply_ip_filter(char* value, int filter_exclude_flag) {
}
// setzen des Filters
strcpy(filters.ip_filters[filters.ip_count].ip_address, value);
strncpy(filters.ip_filters[filters.ip_count].ip_address, value,sizeof(filters.ip_filters[filters.ip_count].ip_address));
filters.ip_filters[filters.ip_count].filter_exclude_flag = filter_exclude_flag;
filters.ip_count++;
@ -921,7 +955,7 @@ void apply_method_filter(char* value, int filter_exclude_flag) {
return;
}
strcpy(filters.method_filters[filters.method_count].pattern, value);
strncpy(filters.method_filters[filters.method_count].pattern,value, sizeof(filters.method_filters[filters.method_count].pattern));
filters.method_filters[filters.method_count].filter_exclude_flag = filter_exclude_flag;
filters.method_count++;
@ -940,7 +974,7 @@ void apply_useragent_filter(char* value, int filter_exclude_flag) {
return;
}
strcpy(filters.user_agent_filters[filters.user_agent_count].pattern, value);
strncpy(filters.user_agent_filters[filters.user_agent_count].pattern, value,sizeof(filters.user_agent_filters[filters.user_agent_count].pattern));
filters.user_agent_filters[filters.user_agent_count].filter_exclude_flag = filter_exclude_flag;
filters.user_agent_count++;
@ -959,7 +993,7 @@ void apply_url_filter(char* value, int filter_exclude_flag) {
return;
}
strcpy(filters.url_filters[filters.url_count].pattern, value);
strncpy(filters.url_filters[filters.url_count].pattern, value,sizeof(filters.url_filters[filters.url_count].pattern));
filters.url_filters[filters.url_count].filter_exclude_flag = filter_exclude_flag;
filters.url_count++;
@ -1054,6 +1088,7 @@ void print_help(char* binary) {
printf("NGINX-Auditor (Beleg)\n");
printf("Nutzung: %s <LOGFILE|VERZEICHNIS> [Flags]\n\n", binary);
printf("Flags:\n");
printf(" -i [Logfile|Verzeichnis] Optional Eingabe definieren - Datei oder Verzeichnis; Standard /var/log/nginx\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");
@ -1068,82 +1103,65 @@ void print_help(char* binary) {
}
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
}
char* input_path = "/var/log/nginx"; // Standardpfad
char export_filename[50];
int flag_export = 0;
int flag_help = 0;
// int flag_verbose = 0; - global definiert
char export_filename[90];
int flag_has_filename = 0;
int flag_filter =0;
int flag_has_export_filename = 0;
int flag_showpreview =0;
allocate_initial_memory();
if (argc >= 2){
// hier wird das offset angewendet
for (int i=arg_offset; i<argc; i++) {
if (strcmp(argv[i], "-e")==0) {
for (int i=1; i<argc; i++){
if (i==0){
continue;
}
if (starts_with(argv[i], "-i")){
if (!starts_with(argv[i+1], "-")&& i+1<argc){
input_path = argv[i+1];
}
}
if (starts_with(argv[i], "-e")){
flag_export =1;
if (i+1<argc && argv[i+1][0]!='-'){
if (!starts_with(argv[i+1], "-")&& i+1<argc){
strncpy(export_filename, argv[i + 1], sizeof(export_filename) - 1);
flag_has_filename = 1;
i++; // Schleife weiter iterieren
}
} else if (strcmp(argv[i], "-f")==0) {
// parsen der nachfolgenden Argumente --ip= usw.
for (int j = i + 1; j < argc; j++) {
if (starts_with(argv[j], "--")) {
parse_filter_argument(argv[j]);
}else{
break;
flag_has_export_filename =1;
}
}
} else if (strcmp(argv[i], "-h")==0) {
flag_help = 1;
} else if (strcmp(argv[i], "-v")==0) {
flag_verbose = 1;
if (starts_with(argv[i], "-f")){
flag_filter = 1;
}
if (starts_with(argv[i], "--")){
parse_filter_argument(argv[i]);
}
if (starts_with(argv[i], "-v")){
flag_verbose =1; // globale Variable oben definiert
}
// Aktionen basierend auf gesetzten flags ausführen
if (flag_help == 1){
if (starts_with(argv[i], "-s")){
flag_showpreview =1;
printf("Flag for show prewview set\n");
}
if (starts_with(argv[i], "-h")){
print_help(argv[0]);
return 1;
} else if (flag_export == 1) {
}
}
load_log_file(input_path);
if (flag_has_filename == 1) {
if (flag_export ==1){
if (flag_has_export_filename){
export_filtered_entries(export_filename);
} else {
export_filtered_entries(NULL);
}
} else {
// Standard-Verhalten: Logs laden und Zusammenfassung anzeigen
load_log_file(input_path);
int filtered_count = count_filtered_entries();
printf("Geladen: %d Einträge, %d entsprechen den Filtern. Nutzen Sie -e zum Exportieren.\n",
total_entries, filtered_count);
printf("Geladen: %d Einträge, %d entsprechen den Filtern. Nutzen Sie -e zum Exportieren.\n", total_entries, filtered_count);
}
if (flag_showpreview==1){
printf("should show preview here\n");
show_preview();
}
cleanup_memory();
printf("INFO: Ausführung beendet.\n");
return 0;
}