diff --git a/.github/workflows/build_test_ci.yml b/.github/workflows/build_test_ci.yml index aecfb6a..d5e8228 100644 --- a/.github/workflows/build_test_ci.yml +++ b/.github/workflows/build_test_ci.yml @@ -23,42 +23,78 @@ jobs: run: gcc -o suricata-notify suricata-notify.c -ljansson - name: Upload Build Artifact - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: suricata-notify path: suricata-notify retention-days: 30 - download: + test: runs-on: ubuntu-latest needs: build steps: - name: Download Artifact - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: suricata-notify path: ./ + - name: Install Dependencies run: | - sudo apt-get update - sudo apt-get install -y build-essential xvfb libnotify-bin strace - - - name: List Downloaded Files - run: ls -la + sudo apt-get update + # sudo apt-get install -y xvfb strace libnotify-bin dbus-x11 + sudo apt-get install -y xvfb xfce4-notifyd x11-apps x11-utils strace libnotify-bin dbus-x11 - name: Make Executable run: chmod +x suricata-notify + - name: Show Help + run: ./suricata-notify --help + - name: Create Test Data - run: echo '[{"event_type":"alert","timestamp":"2023-08-02T00:05:06.384656+0200","alert":{"signature":"Test Signature 1","category":"Test Category 1"}},{"event_type":"alert","timestamp":"2023-08-02T00:06:00.000000+0200","alert":{"signature":"Test Signature 2","category":"Test Category 2"}},{"event_type":"alert","timestamp":"2023-08-02T00:07:00.000000+0200","alert":{"signature":"Test Signature 3","category":"Test Category 3"}}]' > eve.json + run: | + current_time=$(date --utc --date="-33 seconds" +"%Y-%m-%dT%H:%M:%S.%6NZ") + echo "Current Time: $current_time" + # Create the JSON data with the timestamp embedded + json_data='{"timestamp":"'"$current_time"'","flow_id":1234567890123456,"pcap_cnt":150,"event_type":"alert","src_ip":"192.168.1.100","src_port":8080,"dest_ip":"10.0.0.5","dest_port":80,"proto":"TCP","pkt_src":"wire/pcap","ether":{"src_mac":"00:11:22:33:44:55","dest_mac":"66:77:88:99:AA:BB"},"tx_id":2,"alert":{"action":"allowed","gid":1,"signature_id":1000001,"rev":1,"signature":"Test Signature Example","category":"Test Category","severity":2,"metadata":{"affected_product":["Linux_Server_64_Bit"],"attack_target":["Server_Endpoint"],"created_at":["2024_08_20"],"deployment":["Perimeter"],"former_category":["TEST_RESPONSE"],"signature_severity":["Minor"],"updated_at":["2024_08_20"]}},"http":{"hostname":"example.com","http_port":8080,"url":"/test","http_content_type":"application/json","http_method":"GET","protocol":"HTTP/1.1","status":200,"length":1024},"files":[{"filename":"/test","gaps":false,"state":"CLOSED","stored":false,"size":1024,"tx_id":2}],"app_proto":"http","direction":"to_server","flow":{"pkts_toserver":10,"pkts_toclient":8,"bytes_toserver":2048,"bytes_toclient":4096,"start":"2024-08-20T15:29:50.000000+0000","src_ip":"10.0.0.5","dest_ip":"192.168.1.100","src_port":80,"dest_port":8080}}' + echo "$json_data" + echo "$json_data" > eve.json + echo "Test data written to eve.json" - - name: Prepare Virtual Display - run: xvfb-run -a -s "-screen 0 1024x768x24" sh -c "strace -o strace.log ./suricata-notify eve.json" + - name: Prepare Virtual Display and Test Notifications + run: | + # Run the program in a virtual display and capture strace logs + # export DISPLAY=:99 && xvfb-run -a -s "-screen 0 1024x768x24" sh -c ' + export DISPLAY=:0 && xvfb-run -a -s "-screen 0 1024x768x24" sh -c ' + echo "Starting dbus-session..."; + eval $(dbus-launch --sh-syntax --exit-with-session); + echo "Running suricata-notify with strace..." + strace -f -o /tmp/suricata-notify-strace.log ./suricata-notify -v -w 60 -z 0 -e eve.json + ' - name: Upload Strace Log - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: strace-log - path: strace.log + path: /tmp/suricata-notify-strace.log retention-days: 30 + + - name: Check Strace Log + run: | + # Check for specific output in the strace log to determine success + + # Check if the log contains the specific message we expect to send with notify-send + if grep -q "Test Category" /tmp/suricata-notify-strace.log; then + # Check if notify-send exited successfully + if grep -q "notify-send failed" /tmp/suricata-notify-strace.log; then + echo "Notification test failed due to notify-send error" + exit 1 + else + echo "Notification test passed" + exit 0 + fi + else + echo "Notification test failed: Message not found in strace log" + exit 1 + fi diff --git a/.vscode/c_cpp_properties.json b/.vscode/c_cpp_properties.json new file mode 100644 index 0000000..8916bf7 --- /dev/null +++ b/.vscode/c_cpp_properties.json @@ -0,0 +1,19 @@ +{ + "configurations": [ + { + "name": "Mac", + "includePath": [ + "${workspaceFolder}/**" + ], + "defines": [], + "macFrameworkPath": [ + "/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk/System/Library/Frameworks" + ], + "compilerPath": "/usr/bin/clang", + "cStandard": "c17", + "cppStandard": "c++17", + "intelliSenseMode": "macos-clang-arm64" + } + ], + "version": 4 +} \ No newline at end of file diff --git a/suricata-notify.c b/suricata-notify.c index e6f3c2c..a1fc16b 100644 --- a/suricata-notify.c +++ b/suricata-notify.c @@ -4,22 +4,49 @@ #include #include #include +#include #include #include #include -#define MAX_LINE_LENGTH 4096 -#define TIMEZONE_OFFSET_SECONDS 3600 -#define ALERT_WINDOW_SECONDS 60 +#define DEFAULT_MAX_LINE_LENGTH 4096 +#define DEFAULT_TIMEZONE_OFFSET_SECONDS 3600 +#define DEFAULT_ALERT_WINDOW_SECONDS 60 + +// Global variables for configuration +int verbose = 0; +size_t max_line_length = DEFAULT_MAX_LINE_LENGTH; +int timezone_offset_seconds = DEFAULT_TIMEZONE_OFFSET_SECONDS; +int alert_window_seconds = DEFAULT_ALERT_WINDOW_SECONDS; // Function prototypes void send_notification(const char *alert_message); time_t convert_iso8601_to_unix(const char *iso8601_timestamp); void process_alerts(const char *log_file); +void print_help(void); + +// Add the help message function +void print_help(void) +{ + printf("Usage: suricata-notify [options]\n"); + printf("Options:\n"); + printf(" -h, --help Show this help message and exit\n"); + printf(" -v, --verbose Enable verbose output\n"); + printf(" -t, --test Run in test mode (send a test notification)\n"); + printf(" -e, --eve-json Specify the Suricata EVE JSON log file (default: /var/log/suricata/eve.json)\n"); + printf(" -l, --line-length Set the maximum line length for reading the log file (default: %zu)\n", DEFAULT_MAX_LINE_LENGTH); + printf(" -z, --timezone-offset Set the timezone offset in seconds (default: %d)\n", DEFAULT_TIMEZONE_OFFSET_SECONDS); + printf(" -w, --alert-window Set the alert window in seconds (default: %d)\n", DEFAULT_ALERT_WINDOW_SECONDS); +} // Function to send a desktop notification with signature and category void send_notification(const char *alert_message) { + if (verbose) + { + printf("[DEBUG] Sending notification: %s\n", alert_message); + } + pid_t pid = fork(); if (pid < 0) @@ -51,13 +78,42 @@ time_t convert_iso8601_to_unix(const char *iso8601_timestamp) struct tm tm_time; memset(&tm_time, 0, sizeof(struct tm)); - if (strptime(iso8601_timestamp, "%Y-%m-%dT%H:%M:%S.%6N%z", &tm_time) == NULL) + if (verbose) + { + printf("[DEBUG] Converting timestamp: %s\n", iso8601_timestamp); + } + + // Parse the timestamp up to seconds + if (strptime(iso8601_timestamp, "%Y-%m-%dT%H:%M:%S", &tm_time) == NULL) { fprintf(stderr, "Failed to parse timestamp: %s\n", iso8601_timestamp); return (time_t)-1; } - return mktime(&tm_time) - TIMEZONE_OFFSET_SECONDS; + // Convert to time_t (Unix timestamp) + time_t converted_time = mktime(&tm_time); + + if (verbose) + { + printf("[DEBUG] Converted time: %ld\n", (long)converted_time); + } + + return converted_time; +} + +void get_iso8601_timestamp(char *buffer, size_t buffer_size) +{ + struct timeval tv; + gettimeofday(&tv, NULL); // Get the current time including microseconds + + struct tm tm_info; + gmtime_r(&tv.tv_sec, &tm_info); // Convert to UTC + + // Format date and time including seconds + strftime(buffer, buffer_size, "%Y-%m-%dT%H:%M:%S", &tm_info); + + // Append microseconds and timezone + snprintf(buffer + strlen(buffer), buffer_size - strlen(buffer), ".%06ld+0000", tv.tv_usec); } // Function to process Suricata alerts and trigger notifications @@ -70,11 +126,37 @@ void process_alerts(const char *log_file) return; } - char line[MAX_LINE_LENGTH]; + if (verbose) + { + printf("[DEBUG] Processing alerts from log file: %s\n", log_file); + } + + char *line = (char *)malloc(max_line_length); + if (line == NULL) + { + perror("Error allocating memory for line buffer"); + fclose(file); + return; + } + time_t current_time = time(NULL); // Get the current time - while (fgets(line, sizeof(line), file) != NULL) + if (verbose) + { + printf("[DEBUG] Current Time: %ld\n", (long)current_time); + char iso8601_timestamp[40]; + get_iso8601_timestamp(iso8601_timestamp, sizeof(iso8601_timestamp)); + + printf("[DEBUG] Current Time: %s\n", iso8601_timestamp); + } + + while (fgets(line, max_line_length, file) != NULL) { + if (verbose) + { + printf("[DEBUG] Reading line: %s\n", line); + } + // Load the JSON object from the line json_error_t error; json_t *root = json_loads(line, 0, &error); @@ -85,62 +167,208 @@ void process_alerts(const char *log_file) continue; } + if (verbose) + { + printf("[DEBUG] Successfully parsed JSON object.\n"); + } + // Check if the JSON object has the "event_type" field and it is "alert" json_t *event_type = json_object_get(root, "event_type"); - if (json_is_string(event_type) && strcmp(json_string_value(event_type), "alert") == 0) + if (event_type && json_is_string(event_type)) { - // Extract the timestamp - json_t *alert_timestamp_json = json_object_get(root, "timestamp"); - if (json_is_string(alert_timestamp_json)) + if (verbose) { - time_t alert_timestamp = convert_iso8601_to_unix(json_string_value(alert_timestamp_json)); + printf("[DEBUG] Event type found: %s\n", json_string_value(event_type)); + } - // Check if the alert occurred within the last ALERT_WINDOW_SECONDS - if (difftime(current_time, alert_timestamp) <= ALERT_WINDOW_SECONDS) + if (strcmp(json_string_value(event_type), "alert") == 0) + { + // Extract the timestamp + json_t *alert_timestamp_json = json_object_get(root, "timestamp"); + if (alert_timestamp_json && json_is_string(alert_timestamp_json)) { - json_t *alert = json_object_get(root, "alert"); - if (json_is_object(alert)) + if (verbose) + { + printf("[DEBUG] Alert timestamp found: %s\n", json_string_value(alert_timestamp_json)); + } + + time_t alert_timestamp = convert_iso8601_to_unix(json_string_value(alert_timestamp_json)); + + // Check if the alert occurred within the last ALERT_WINDOW_SECONDS + if (difftime(current_time, alert_timestamp) <= alert_window_seconds) { - // Extract the signature and category - json_t *signature_json = json_object_get(alert, "signature"); - json_t *category_json = json_object_get(alert, "category"); + if (verbose) + { + double diff = difftime(current_time, alert_timestamp); + printf("[DEBUG] Alert occurred within the last %d seconds with a diff of %.0f seconds.\n", alert_window_seconds, diff); + } + + json_t *alert = json_object_get(root, "alert"); + if (alert && json_is_object(alert)) + { + // Extract the signature and category + json_t *signature_json = json_object_get(alert, "signature"); + json_t *category_json = json_object_get(alert, "category"); + + if (signature_json && json_is_string(signature_json) && json_is_string(category_json)) + { + if (verbose) + { + printf("[DEBUG] Alert signature: %s\n", json_string_value(signature_json)); + printf("[DEBUG] Alert category: %s\n", json_string_value(category_json)); + } + + // Create the alert message + char alert_message[max_line_length]; + snprintf(alert_message, sizeof(alert_message), "Category: %s\nSignature: %s", json_string_value(category_json), json_string_value(signature_json)); - if (json_is_string(signature_json) && json_is_string(category_json)) + if (verbose) + { + printf("[DEBUG] Sending notification: %s\n", alert_message); + } + + send_notification(alert_message); + } + else + { + if (verbose) + { + printf("[DEBUG] Missing or invalid 'signature' or 'category' field in alert.\n"); + } + } + } + else { - // Create the alert message - char alert_message[MAX_LINE_LENGTH]; - snprintf(alert_message, sizeof(alert_message), "Category: %s\nSignature: %s", json_string_value(category_json), json_string_value(signature_json)); - send_notification(alert_message); + if (verbose) + { + printf("[DEBUG] 'alert' field is missing or is not an object.\n"); + } + } + } + else + { + if (verbose) + { + printf("[DEBUG] Alert is older than %d seconds, skipping notification.\n", alert_window_seconds); } } } + else + { + if (verbose) + { + printf("[DEBUG] Missing or invalid 'timestamp' field in alert.\n"); + } + } + } + } + else + { + if (verbose) + { + printf("[DEBUG] 'event_type' field is missing or is not a string.\n"); } } json_decref(root); } + if (verbose) + { + printf("[DEBUG] Finished processing alerts.\n"); + } + + free(line); fclose(file); } int main(int argc, char *argv[]) { int is_test = 0; + const char *suricata_log = "/var/log/suricata/eve.json"; // Default log file path - // Check if the flag for testing mode is passed - if (argc > 1 && strcmp(argv[1], "--test") == 0) + for (int i = 1; i < argc; i++) { - is_test = 1; + if (strcmp(argv[i], "-h") == 0 || strcmp(argv[i], "--help") == 0) + { + print_help(); + return EXIT_SUCCESS; + } + else if (strcmp(argv[i], "-v") == 0 || strcmp(argv[i], "--verbose") == 0) + { + verbose = 1; + } + else if (strcmp(argv[i], "-t") == 0 || strcmp(argv[i], "--test") == 0) + { + is_test = 1; + } + else if (strcmp(argv[i], "-e") == 0 || strcmp(argv[i], "--eve-json") == 0) + { + if (i + 1 < argc) + { + suricata_log = argv[i + 1]; // Get the log file name + i++; // Skip the log file argument + } + else + { + fprintf(stderr, "Error: Missing argument for %s\n", argv[i]); + return EXIT_FAILURE; + } + } + else if (strcmp(argv[i], "-l") == 0 || strcmp(argv[i], "--line-length") == 0) + { + if (i + 1 < argc) + { + max_line_length = strtoul(argv[i + 1], NULL, 10); + i++; + } + else + { + fprintf(stderr, "Error: Missing argument for %s\n", argv[i]); + return EXIT_FAILURE; + } + } + else if (strcmp(argv[i], "-z") == 0 || strcmp(argv[i], "--timezone-offset") == 0) + { + if (i + 1 < argc) + { + timezone_offset_seconds = atoi(argv[i + 1]); + i++; + } + else + { + fprintf(stderr, "Error: Missing argument for %s\n", argv[i]); + return EXIT_FAILURE; + } + } + else if (strcmp(argv[i], "-w") == 0 || strcmp(argv[i], "--alert-window") == 0) + { + if (i + 1 < argc) + { + alert_window_seconds = atoi(argv[i + 1]); + i++; + } + else + { + fprintf(stderr, "Error: Missing argument for %s\n", argv[i]); + return EXIT_FAILURE; + } + } + else + { + fprintf(stderr, "Unknown argument: %s\n", argv[i]); + return EXIT_FAILURE; + } } if (is_test) { - // Simulate notifications by logging + // Test mode doesn't need the log file, so ignore the -e argument if present send_notification("Test notification"); } else { - const char *suricata_log = (argc > 1) ? argv[1] : "/var/log/suricata/eve.json"; + // Process the alerts from the specified log file process_alerts(suricata_log); }