#!/bin/bash ################################################################################ # Script Name: wazuh-exporter.sh # Version: 1.0 # Description: Prometheus exporter for Wazuh SIEM providing operational # metrics via the Wazuh API — agent status, manager daemon health, # active alerts, vulnerability counts, rule and decoder totals, # syscheck events, and API health # # Author: Phil Connor # Contact: contact@mylinux.work # Website: https://mylinux.work # License: MIT # # Prerequisites: # - Wazuh Manager installed and running # - Wazuh API accessible (default: https://localhost:55000) # - curl for API calls # - jq for JSON parsing # - netcat (nc) for HTTP mode # # Usage: # # Output to stdout # ./wazuh-exporter.sh # # # HTTP server mode # ./wazuh-exporter.sh --http -p 9193 # # # Textfile collector mode # ./wazuh-exporter.sh --textfile # # # Custom API credentials # ./wazuh-exporter.sh --api-user admin --api-pass secret # # Metrics Exported: # - wazuh_up - API reachability (1=up, 0=down) # - wazuh_info{version,cluster_name} - Wazuh version info # - wazuh_agents_total - Total agent count # - wazuh_agents_by_status{status} - Agents by status # - wazuh_agents_by_os{os} - Agents by OS platform # - wazuh_manager_status{daemon} - Per-daemon status (1=running, 0=stopped) # - wazuh_manager_log_errors - Error log entry count # - wazuh_manager_log_warnings - Warning log entry count # - wazuh_rules_total - Total rules loaded # - wazuh_decoders_total - Total decoders loaded # - wazuh_alerts_total_today - Total alerts today # - wazuh_syscheck_events_today - Syscheck (FIM) events today # - wazuh_vulnerability_detector_critical - Critical vulnerabilities # - wazuh_vulnerability_detector_high - High severity vulnerabilities # - wazuh_exporter_duration_seconds - Script execution time # - wazuh_exporter_last_run_timestamp - Last run timestamp # # Configuration: # Default HTTP port: 9193 # Default API URL: https://localhost:55000 # Default API user: wazuh # Default API pass: wazuh # Textfile directory: /var/lib/node_exporter # ################################################################################ set -euo pipefail # ============================================================================ # CONFIGURATION VARIABLES # ============================================================================ TEXTFILE_DIR="/var/lib/node_exporter" OUTPUT_FILE="" HTTP_MODE=false HTTP_PORT=9193 API_URL="https://localhost:55000" API_USER="wazuh" API_PASS="wazuh" # JWT token cache (populated on first API call) JWT_TOKEN="" # ============================================================================ # HELPER FUNCTIONS # ============================================================================ show_usage() { cat <&2; exit 1 ;; esac done } # Check prerequisites # Returns: 0 if OK, 1 if error check_prerequisites() { if ! command -v curl >/dev/null 2>&1; then echo "ERROR: curl not found" >&2 return 1 fi if ! command -v jq >/dev/null 2>&1; then echo "ERROR: jq not found (required for JSON parsing)" >&2 return 1 fi return 0 } # Escape special characters in Prometheus label values # Args: $1 - string to escape # Returns: escaped string safe for Prometheus labels prom_escape() { local val="$1" val="${val//\\/\\\\}" val="${val//\"/\\\"}" val="${val//$'\n'/}" echo "$val" } # Authenticate against Wazuh API and cache JWT token # Returns: 0 if authenticated, 1 if error authenticate() { if [ -n "$JWT_TOKEN" ]; then return 0 fi local response response=$(curl -s -k -X POST \ -u "${API_USER}:${API_PASS}" \ "${API_URL}/security/user/authenticate" 2>/dev/null) || return 1 JWT_TOKEN=$(echo "$response" | jq -r '.data.token // empty' 2>/dev/null) if [ -z "$JWT_TOKEN" ]; then echo "ERROR: Failed to authenticate with Wazuh API" >&2 return 1 fi return 0 } # Make an authenticated API call # Args: $1 - API endpoint path (e.g., /agents/summary/status) # Returns: JSON response on stdout api_call() { local endpoint="$1" curl -s -k -X GET \ -H "Authorization: Bearer ${JWT_TOKEN}" \ "${API_URL}${endpoint}" 2>/dev/null } # ============================================================================ # METRIC GENERATION # ============================================================================ # Generate all Prometheus metrics # Returns: Prometheus text format metrics on stdout generate_metrics() { local script_start script_start=$(date +%s) # Check prerequisites if ! check_prerequisites; then cat </dev/null) if [ -z "$root_response" ]; then cat </dev/null) cluster_name=$(echo "$root_response" | jq -r '.data.cluster_name // "unknown"' 2>/dev/null) cat </dev/null) disconnected=$(echo "$agents_summary" | jq -r '.data.connection.disconnected // 0' 2>/dev/null) never_connected=$(echo "$agents_summary" | jq -r '.data.connection.never_connected // 0' 2>/dev/null) pending=$(echo "$agents_summary" | jq -r '.data.connection.pending // 0' 2>/dev/null) total=$(echo "$agents_summary" | jq -r '.data.connection.total // 0' 2>/dev/null) cat </dev/null | while read -r os count; do [ -z "$os" ] && continue echo "wazuh_agents_by_os{os=\"$(prom_escape "$os")\"} $count" done fi echo "" # ======================================================================== # MANAGER METRICS # ======================================================================== local manager_status manager_status=$(api_call "/manager/status") cat </dev/null | while read -r daemon status; do [ -z "$daemon" ] && continue local val=0 if [ "$status" = "running" ]; then val=1 fi echo "wazuh_manager_status{daemon=\"$(prom_escape "$daemon")\"} $val" done fi echo "" # Manager logs summary local logs_summary logs_summary=$(api_call "/manager/logs/summary") local total_errors=0 local total_warnings=0 if [ -n "$logs_summary" ] && [ "$logs_summary" != "null" ]; then total_errors=$(echo "$logs_summary" | jq '[.data.affected_items[0] // {} | to_entries[] | .value.error // 0] | add // 0' 2>/dev/null) total_warnings=$(echo "$logs_summary" | jq '[.data.affected_items[0] // {} | to_entries[] | .value.warning // 0] | add // 0' 2>/dev/null) total_errors=${total_errors:-0} total_warnings=${total_warnings:-0} fi cat </dev/null) total_rules=${total_rules:-0} fi cat </dev/null) total_decoders=${total_decoders:-0} fi cat </dev/null) total_syscheck_today=$(echo "$stats_response" | jq '[.data.affected_items[] | .syscheck // 0] | add // 0' 2>/dev/null) total_alerts_today=${total_alerts_today:-0} total_syscheck_today=${total_syscheck_today:-0} fi cat </dev/null) critical_vulns=${critical_vulns:-0} fi if [ -n "$vuln_high_response" ] && [ "$vuln_high_response" != "null" ]; then high_vulns=$(echo "$vuln_high_response" | jq '.data.total_affected_items // 0' 2>/dev/null) high_vulns=${high_vulns:-0} fi cat <&2 if ! command -v nc >/dev/null 2>&1; then echo "ERROR: netcat (nc) required for HTTP mode" >&2 exit 1 fi # Infinite loop accepting HTTP requests while true; do { read -r request # Check if request is for /metrics endpoint 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 # Serve HTML landing page for other requests echo -e "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\n\r" cat < Wazuh Exporter v1.0

Wazuh Prometheus Exporter v1.0

Metrics

Operational metrics from the Wazuh API.

EOF fi } | nc -l -p "$HTTP_PORT" -q 1 2>/dev/null done } # ============================================================================ # MAIN EXECUTION # ============================================================================ # Main entry point - routes to appropriate output mode main() { parse_args "$@" if [ "$HTTP_MODE" = true ]; then # Run HTTP server (blocks until killed) run_http_server elif [ -n "$OUTPUT_FILE" ]; then # Textfile collector mode: write atomically using temp file local output_dir output_dir="$(dirname "$OUTPUT_FILE")" mkdir -p "$output_dir" # Create temp file in SAME directory for atomic rename (same filesystem) local temp_file temp_file=$(mktemp "${output_dir}/.wazuh_metrics.XXXXXX") # Generate metrics to temp file if ! generate_metrics > "$temp_file" 2>/dev/null; then rm -f "$temp_file" echo "ERROR: Failed to generate metrics" >&2 exit 1 fi # Validate: file must exist, have content local file_lines file_lines=$(wc -l < "$temp_file" 2>/dev/null || echo 0) if [ "$file_lines" -lt 10 ]; then rm -f "$temp_file" echo "ERROR: Metrics file too small ($file_lines lines), keeping previous" >&2 exit 1 fi # Set permissions before move chmod 644 "$temp_file" # Atomic rename - no gap where file is missing mv -f "$temp_file" "$OUTPUT_FILE" echo "Metrics written to $OUTPUT_FILE ($file_lines lines)" >&2 else # Default: output to stdout generate_metrics fi } # Execute main function with all script arguments main "$@"