#!/bin/bash ################################################################################ # Script Name: suricata-exporter.sh # Version: 1.0 # Description: Prometheus exporter for Suricata IDS/IPS — alert counts by # severity, top signatures, protocol breakdown, flow stats, # drop counts, decoder errors, and eve.json processing metrics # # Author: Phil Connor # Contact: contact@mylinux.work # Website: https://mylinux.work # License: MIT # # Prerequisites: # - Suricata installed and running # - jq for JSON parsing # - Access to eve.json log and suricata.socket # - Root/sudo access # # Usage: # # Output to stdout # sudo ./suricata-exporter.sh # # # HTTP server mode # sudo ./suricata-exporter.sh --http -p 9196 # # # Textfile collector mode # sudo ./suricata-exporter.sh --textfile # # Metrics Exported: # - suricata_up - Exporter status (1=up, 0=down) # - suricata_exporter_info{version} - Exporter version info # - suricata_alerts_total - Total alerts from eve.json # - suricata_alerts_by_severity{severity} - Alerts by severity level # - suricata_alerts_by_category{category} - Alerts by classification # - suricata_top_signature_alerts{signature,sid} - Top triggered signatures # - suricata_flows_total - Total tracked flows # - suricata_flows_by_protocol{protocol} - Flows by protocol # - suricata_packets_total - Total packets processed # - suricata_bytes_total - Total bytes processed # - suricata_drops_total - Total dropped packets (IPS mode) # - suricata_decoder_errors_total - Decoder error count # - suricata_detect_alerts_total - Detection engine alert count # - suricata_uptime_seconds - Suricata process uptime # - suricata_memory_bytes - Suricata memory usage # - suricata_rules_loaded - Number of loaded rules # - suricata_rules_failed - Number of rules that failed to load # - suricata_capture_kernel_drops - Kernel-level packet drops # - suricata_exporter_duration_seconds - Script execution time # - suricata_exporter_last_run_timestamp - Last run timestamp # # Configuration: # Default HTTP port: 9196 # Textfile directory: /var/lib/node_exporter # Eve log: /var/log/suricata/eve.json # ################################################################################ # ============================================================================ # CONFIGURATION VARIABLES # ============================================================================ TEXTFILE_DIR="/var/lib/node_exporter" OUTPUT_FILE="" HTTP_MODE=false HTTP_PORT=9196 EVE_LOG="/var/log/suricata/eve.json" SURICATA_SOCKET="/var/run/suricata/suricata-command.socket" LOOKBACK_LINES=50000 EXPORTER_VERSION="1.0" # ============================================================================ # HELPER FUNCTIONS # ============================================================================ show_usage() { cat <&2; exit 1 ;; esac done } prom_escape() { local val="$1" val="${val//\\/\\\\}" val="${val//\"/\\\"}" val="${val//$'\n'/}" echo "$val" } check_suricata() { if ! pgrep -x suricata >/dev/null 2>&1; then return 1 fi return 0 } # ============================================================================ # DATA COLLECTION # ============================================================================ get_suricata_stats() { if [ -S "$SURICATA_SOCKET" ]; then echo "dump-counters" | suricatasc "$SURICATA_SOCKET" 2>/dev/null fi } get_eve_alerts() { if [ -f "$EVE_LOG" ]; then tail -n "$LOOKBACK_LINES" "$EVE_LOG" | jq -c 'select(.event_type == "alert")' 2>/dev/null fi } get_eve_stats() { if [ -f "$EVE_LOG" ]; then tail -n "$LOOKBACK_LINES" "$EVE_LOG" | jq -c 'select(.event_type == "stats")' 2>/dev/null | tail -1 fi } get_suricata_pid() { pgrep -x suricata | head -1 } # ============================================================================ # METRIC GENERATION # ============================================================================ generate_metrics() { local script_start script_start=$(date +%s%N 2>/dev/null || date +%s) if ! check_suricata; then cat </dev/null) btime=$(awk '/^btime/ {print $2}' /proc/stat 2>/dev/null) clk_tck=$(getconf CLK_TCK 2>/dev/null || echo 100) now_seconds=$(date +%s) if [ -n "$starttime" ] && [ -n "$btime" ]; then local start_seconds=$((btime + starttime / clk_tck)) uptime_seconds=$((now_seconds - start_seconds)) fi fi if [ -f "/proc/$pid/status" ]; then memory_bytes=$(awk '/^VmRSS:/ {print $2 * 1024}' "/proc/$pid/status" 2>/dev/null) memory_bytes=${memory_bytes:-0} fi cat </dev/null) capture_bytes=$(echo "$stats_json" | jq '.stats.decoder.bytes // 0' 2>/dev/null) capture_drops=$(echo "$stats_json" | jq '.stats.capture.kernel_drops // 0' 2>/dev/null) decoder_errors=$(echo "$stats_json" | jq '.stats.decoder.invalid // 0' 2>/dev/null) detect_alerts=$(echo "$stats_json" | jq '.stats.detect.alert // 0' 2>/dev/null) cat </dev/null) udp_flows=$(echo "$stats_json" | jq '.stats.flow.udp // 0' 2>/dev/null) icmp_flows=$(echo "$stats_json" | jq '.stats.flow.icmp // 0' 2>/dev/null) local total_flows=$(( ${tcp_flows:-0} + ${udp_flows:-0} + ${icmp_flows:-0} )) cat </dev/null) rules_failed=$(echo "$stats_json" | jq '.stats.detect.engines[0].rules_failed // 0' 2>/dev/null) cat </dev/null | sort | uniq -c | while read -r count severity; do [ -z "$severity" ] && continue echo "suricata_alerts_by_severity{severity=\"$severity\"} $count" done fi echo "" # Alerts by category cat </dev/null | sort | uniq -c | sort -rn | head -10 | while read -r count category; do [ -z "$category" ] && continue echo "suricata_alerts_by_category{category=\"$(prom_escape "$category")\"} $count" done fi echo "" # Top 10 signatures cat </dev/null | sort | uniq -c | sort -rn | head -10 | while read -r count sid sig; do [ -z "$sid" ] && continue echo "suricata_top_signature_alerts{signature=\"$(prom_escape "$sig")\",sid=\"$sid\"} $count" done fi echo "" # ======================================================================== # EXPORTER RUNTIME # ======================================================================== local script_end script_duration script_end=$(date +%s) local start_s=$((script_start / 1000000000)) [ "$start_s" -eq 0 ] && start_s=$script_start script_duration=$((script_end - start_s)) cat <&2 if ! command -v nc >/dev/null 2>&1; then echo "ERROR: netcat (nc) required for HTTP mode" >&2 exit 1 fi while true; do { read -r request if [[ "$request" =~ ^GET\ /metrics ]]; then echo -e "HTTP/1.1 200 OK\r\nContent-Type: text/plain; version=0.0.4\r\n\r" generate_metrics else echo -e "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\n\r" cat < Suricata Exporter v$EXPORTER_VERSION

Suricata Prometheus Exporter v$EXPORTER_VERSION

Metrics

EOF fi } | nc -l -p "$HTTP_PORT" -q 1 2>/dev/null done } # ============================================================================ # MAIN EXECUTION # ============================================================================ main() { parse_args "$@" if [ "$HTTP_MODE" = true ]; then run_http_server elif [ -n "$OUTPUT_FILE" ]; then local output_dir output_dir="$(dirname "$OUTPUT_FILE")" mkdir -p "$output_dir" local temp_file temp_file=$(mktemp "${output_dir}/.suricata_metrics.XXXXXX") if ! generate_metrics > "$temp_file" 2>/dev/null; then rm -f "$temp_file" echo "ERROR: Failed to generate metrics" >&2 exit 1 fi local file_lines file_lines=$(wc -l < "$temp_file" 2>/dev/null || echo 0) if [ "$file_lines" -lt 5 ]; then rm -f "$temp_file" echo "ERROR: Metrics file too small ($file_lines lines)" >&2 exit 1 fi chmod 644 "$temp_file" mv -f "$temp_file" "$OUTPUT_FILE" echo "Metrics written to $OUTPUT_FILE ($file_lines lines)" >&2 else generate_metrics fi } main "$@"