#!/usr/bin/env bash set -euo pipefail # Combined Wazuh simulator script (single entrypoint) # Replays production-style sample logs from samples/*.log to Wazuh syslog UDP. # # Usage: # scripts/send-wazuh-sim-logs.sh [selector] [count] [delay_seconds] [--forever] [--dry-run] [--no-mock] [--no-guarantee-hits] [--random-types] [--include-nonalerts] [--docker-send] # # Selectors: # all # a|b|c|appendix-a|appendix-b|appendix-c # a1|a2|a3|a4|b1|b2|b3|c1|c2|c3 # A1-01, A2-10, B3-06, C1-01, ... SELECTOR="${1:-all}" COUNT="${2:-1}" DELAY="${3:-1}" shift $(( $# >= 3 ? 3 : $# )) || true FOREVER=0 DRY_RUN="${DRY_RUN:-0}" MOCK_VALUES="${MOCK_VALUES:-1}" GUARANTEE_HITS="${GUARANTEE_HITS:-1}" RANDOM_TYPES="${RANDOM_TYPES:-0}" INCLUDE_NONALERTS="${INCLUDE_NONALERTS:-0}" DOCKER_SEND="${DOCKER_SEND:-0}" WAZUH_MANAGER_CONTAINER="${WAZUH_MANAGER_CONTAINER:-wazuh-single-wazuh.manager-1}" for arg in "$@"; do case "$arg" in --forever) FOREVER=1 ;; --dry-run) DRY_RUN=1 ;; --no-mock) MOCK_VALUES=0 ;; --mock) MOCK_VALUES=1 ;; --no-guarantee-hits) GUARANTEE_HITS=0 ;; --guarantee-hits) GUARANTEE_HITS=1 ;; --random-types) RANDOM_TYPES=1 ;; --include-nonalerts) INCLUDE_NONALERTS=1 ;; --docker-send) DOCKER_SEND=1 ;; -h|--help) echo "usage: scripts/send-wazuh-sim-logs.sh [selector] [count] [delay_seconds] [--forever] [--dry-run] [--no-mock] [--no-guarantee-hits] [--random-types] [--include-nonalerts] [--docker-send]" exit 0 ;; *) echo "error: unknown option '$arg'" exit 1 ;; esac done if ! [[ "$COUNT" =~ ^[0-9]+$ ]] || [ "$COUNT" -lt 1 ]; then echo "error: count must be a positive integer" exit 1 fi if ! [[ "$DELAY" =~ ^[0-9]+([.][0-9]+)?$ ]]; then echo "error: delay_seconds must be numeric" exit 1 fi WAZUH_SYSLOG_HOST="${WAZUH_SYSLOG_HOST:-127.0.0.1}" WAZUH_SYSLOG_PORT="${WAZUH_SYSLOG_PORT:-514}" NC_WAIT_SECONDS="${NC_WAIT_SECONDS:-0}" STRICT_SEND="${STRICT_SEND:-1}" BASE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" SAMPLES_DIR="${BASE_DIR}/samples" normalize() { printf '%s' "$1" | tr '[:upper:]' '[:lower:]' } is_valid_selector() { local sel sel="$(normalize "$1")" case "$sel" in all|a|b|c|appendix-a|appendix-b|appendix-c|a1|a2|a3|a4|b1|b2|b3|c1|c2|c3) return 0 ;; [abc][0-9]-[0-9][0-9]) return 0 ;; *) return 1 ;; esac } selector_matches_tag() { local selector tag sel tagl selector="$1" tag="$2" sel="$(normalize "$selector")" tagl="$(normalize "$tag")" if [ -z "$tagl" ]; then case "$sel" in all|a|b|c|appendix-a|appendix-b|appendix-c) return 0 ;; *) return 1 ;; esac fi case "$sel" in all) return 0 ;; a|appendix-a) [[ "$tagl" == a* ]] return ;; b|appendix-b) [[ "$tagl" == b* ]] return ;; c|appendix-c) [[ "$tagl" == c* ]] return ;; a1|a2|a3|a4|b1|b2|b3|c1|c2|c3) [[ "$tagl" == "$sel"-* ]] return ;; [abc][0-9]-[0-9][0-9]) [[ "$tagl" == "$sel" ]] return ;; *) return 1 ;; esac } sample_files_for_selector() { local sel sel="$(normalize "$1")" case "$sel" in all) echo "${SAMPLES_DIR}/appendix-a-production-samples.log" echo "${SAMPLES_DIR}/appendix-b-production-samples.log" echo "${SAMPLES_DIR}/appendix-c-production-samples.log" ;; a|appendix-a|a1|a2|a3|a4|a[0-9]-[0-9][0-9]) echo "${SAMPLES_DIR}/appendix-a-production-samples.log" ;; b|appendix-b|b1|b2|b3|b[0-9]-[0-9][0-9]) echo "${SAMPLES_DIR}/appendix-b-production-samples.log" ;; c|appendix-c|c1|c2|c3|c[0-9]-[0-9][0-9]) echo "${SAMPLES_DIR}/appendix-c-production-samples.log" ;; *) echo "error: unsupported selector '$1'" >&2 return 1 ;; esac } emit_syslog() { local line="$1" if [ "${DRY_RUN}" = "1" ]; then echo "DRY_RUN -> ${line}" return 0 fi if [ "${DOCKER_SEND}" = "1" ]; then if ! printf '%s\n' "${line}" | docker exec -i "${WAZUH_MANAGER_CONTAINER}" bash -lc 'cat > /dev/udp/127.0.0.1/514'; then echo "send_failed target=${WAZUH_MANAGER_CONTAINER}:127.0.0.1:514/udp transport=docker-exec" >&2 if [ "${STRICT_SEND}" = "1" ]; then return 1 fi fi return 0 fi if command -v nc >/dev/null 2>&1; then if ! printf '%s\n' "${line}" | nc -w "${NC_WAIT_SECONDS}" -u "${WAZUH_SYSLOG_HOST}" "${WAZUH_SYSLOG_PORT}"; then echo "send_failed target=${WAZUH_SYSLOG_HOST}:${WAZUH_SYSLOG_PORT}/udp transport=nc" >&2 if [ "${STRICT_SEND}" = "1" ]; then return 1 fi fi else if ! printf '%s\n' "${line}" >"/dev/udp/${WAZUH_SYSLOG_HOST}/${WAZUH_SYSLOG_PORT}"; then echo "send_failed target=${WAZUH_SYSLOG_HOST}:${WAZUH_SYSLOG_PORT}/udp transport=devudp" >&2 if [ "${STRICT_SEND}" = "1" ]; then return 1 fi fi fi } rand_between() { local min max min="$1" max="$2" echo $(( min + RANDOM % (max - min + 1) )) } random_public_ip() { local range last range="$(rand_between 0 2)" last="$(rand_between 2 254)" case "$range" in 0) echo "198.51.100.${last}" ;; 1) echo "203.0.113.${last}" ;; *) echo "192.0.2.${last}" ;; esac } random_private_ip() { echo "10.$(rand_between 10 30).$(rand_between 1 254).$(rand_between 1 254)" } random_user() { local users=( "admin01" "analyst01" "helpdesk01" "ops.admin" "jane.doe" "svc_backup$" "svc_dbbackup$" "finance.user" "it-admin" "guest" ) echo "${users[$((RANDOM % ${#users[@]}))]}" } random_fgt_model() { local models=("FGT40F-Branch01" "FGT60F-Branch01" "FGT80F-Branch01" "FGT501E-DC01") echo "${models[$((RANDOM % ${#models[@]}))]}" } random_devid() { local n n="$(rand_between 10000000 99999999)" echo "FGT80FTK${n}" } random_domain() { echo "ioc-$(rand_between 1000 9999).malicious.example" } replace_kv() { local input key new input="$1" key="$2" new="$3" printf '%s' "$input" | sed -E "s#${key}=\"[^\"]*\"#${key}=\"${new}\"#g; s#${key}=([^\" ][^ ]*)#${key}=${new}#g" } mock_windows_json_line() { local line src_ip line="$1" src_ip="$(random_public_ip)" if command -v jq >/dev/null 2>&1; then printf '%s' "$line" | jq -c \ --arg srcip "$src_ip" \ ' if (.win.eventdata | type) == "object" then .win.eventdata |= ( if has("subjectUserName") then .subjectUserName = "SYSTEM" else . end | if has("workstationName") then .workstationName = ("WS-" + (($srcip|split("."))[3])) else . end | if has("ipAddress") then .ipAddress = $srcip else . end ) else . end ' 2>/dev/null || printf '%s' "$line" else printf '%s' "$line" fi } mock_non_json_line() { local line now_date now_time now_iso epoch src_pub dst_priv prev_pub devname devid query line="$1" now_date="$(date '+%Y-%m-%d')" now_time="$(date '+%H:%M:%S')" now_iso="$(date -u '+%Y-%m-%dT%H:%M:%S.000Z')" epoch="$(date '+%s')" src_pub="$(random_public_ip)" dst_priv="$(random_private_ip)" prev_pub="$(random_public_ip)" devname="$(random_fgt_model)" devid="$(random_devid)" query="$(random_domain)" line="$(replace_kv "$line" "date" "$now_date")" line="$(replace_kv "$line" "time" "$now_time")" line="$(replace_kv "$line" "eventtime" "$epoch")" line="$(replace_kv "$line" "devname" "$devname")" line="$(replace_kv "$line" "devid" "$devid")" line="$(replace_kv "$line" "srcip" "$src_pub")" line="$(replace_kv "$line" "dstip" "$dst_priv")" line="$(replace_kv "$line" "src_ip" "$src_pub")" line="$(replace_kv "$line" "prev_ip" "$prev_pub")" line="$(replace_kv "$line" "query" "$query")" line="$(replace_kv "$line" "srcport" "$(rand_between 1025 65535)")" line="$(replace_kv "$line" "distance_km" "$(rand_between 500 16000)")" line="$(replace_kv "$line" "travel_minutes" "$(rand_between 5 180)")" if [[ "$line" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}T ]]; then line="$(printf '%s' "$line" | sed -E "s#^[0-9]{4}-[0-9]{2}-[0-9]{2}T[^ ]+#${now_iso}#")" fi line="$(printf '%s' "$line" | sed -E "s#from [0-9]{1,3}(\.[0-9]{1,3}){3}#from ${src_pub}#g")" line="$(printf '%s' "$line" | sed -E "s# port [0-9]{2,5}# port $(rand_between 1025 65535)#g")" printf '%s' "$line" } mock_line() { local line="$1" if [ "$MOCK_VALUES" != "1" ]; then printf '%s' "$line" return 0 fi if [[ "$line" =~ ^\{ ]]; then mock_windows_json_line "$line" else mock_non_json_line "$line" fi } send_file_once() { local file selector line sent current_tag extracted file="$1" selector="$2" sent=0 current_tag="" while IFS= read -r line || [ -n "$line" ]; do if [[ "$line" =~ ^[[:space:]]*#[[:space:]]*([A-Za-z][0-9]-[0-9]{2})([[:space:]]|$) ]]; then extracted="${BASH_REMATCH[1]}" current_tag="$(normalize "$extracted")" continue fi if [[ -z "${line// }" ]] || [[ "$line" =~ ^[[:space:]]*# ]]; then continue fi if selector_matches_tag "$selector" "$current_tag"; then line="$(mock_line "$line")" emit_syslog "$line" sent=$((sent + 1)) sleep "$DELAY" fi done < "$file" echo "sent=${sent} file=$(basename "$file") selector=$(normalize "$selector")" } send_guaranteed_hits_once() { local selector sent idx tag line selector="$1" sent=0 local tags=( "a1-01" "a1-02" "a2-02" "a2-03" "a2-05" "a2-10" "c1-01" "c1-01" ) local lines=( "soc_event=dns_ioc event_type=ioc_dns_traffic src_ip=10.26.45.214 query=ioc-2294.malicious.example action=blocked severity=medium" "soc_event=dns_ioc event_type=ioc_domain_match src_ip=10.26.45.214 query=bad-c2.example feed=internal_main confidence=high action=alert" "date=2026-03-09 time=10:02:04 devname=\"FGT80F-Branch01\" devid=\"FGT80FTK20000001\" eventtime=1773079324 vd=\"root\" logid=\"0100044547\" type=\"event\" subtype=\"system\" level=\"warning\" user=\"admin\" action=\"password-change\" ui=\"https(10.20.55.1)\"" "date=2026-03-09 time=10:02:17 devname=\"FGT80F-Branch01\" devid=\"FGT80FTK20000001\" eventtime=1773079337 vd=\"root\" logid=\"0100044548\" type=\"event\" subtype=\"system\" level=\"warning\" user=\"admin\" action=\"create-admin\" target_user=\"soc-backup-admin\"" "date=2026-03-09 time=10:04:03 devname=\"FGT80F-Branch01\" devid=\"FGT80FTK20000001\" eventtime=1773079443 vd=\"root\" logid=\"0100044552\" type=\"event\" subtype=\"system\" level=\"notice\" user=\"admin\" action=\"download-config\" dstip=10.20.50.33" "date=2026-03-09 time=10:07:59 devname=\"FGT80F-Branch01\" devid=\"FGT80FTK20000001\" eventtime=1773079679 vd=\"root\" logid=\"0000000014\" type=\"traffic\" subtype=\"forward\" level=\"warning\" srcip=10.20.55.50 dstip=203.0.113.60 dstport=443 threat_label=\"known-c2\" action=\"accept\"" "date=2026-03-09 time=10:31:00 devname=\"FGT80F-Branch01\" devid=\"FGT80FTK20000001\" eventtime=1773081060 vd=\"root\" logid=\"0101037135\" type=\"event\" subtype=\"vpn\" tunneltype=\"ssl\" action=\"ssl-login-success\" user=\"analyst01\" srcip=203.0.113.71 previous_country=TH current_country=US" "soc_event=correlation event_type=c1_impossible_travel user=\"analyst01\" src_ip=203.0.113.71 prev_ip=203.0.113.11 prev_country=TH current_country=US distance_km=13890 travel_minutes=18" ) for idx in "${!lines[@]}"; do tag="${tags[$idx]}" if selector_matches_tag "$selector" "$tag"; then line="$(mock_line "${lines[$idx]}")" emit_syslog "$line" sent=$((sent + 1)) sleep "$DELAY" fi done echo "guaranteed_sent=${sent} selector=$(normalize "$selector")" } EVENT_POOL_READY=0 EVENT_POOL_SELECTOR="" declare -a EVENT_POOL_TAGS declare -a EVENT_POOL_LINES build_event_pool() { local selector file line current_tag extracted idx tag selector="$1" EVENT_POOL_READY=0 EVENT_POOL_SELECTOR="$selector" EVENT_POOL_TAGS=() EVENT_POOL_LINES=() if [ "$INCLUDE_NONALERTS" = "1" ]; then for file in "${FILES[@]}"; do current_tag="" while IFS= read -r line || [ -n "$line" ]; do if [[ "$line" =~ ^[[:space:]]*#[[:space:]]*([A-Za-z][0-9]-[0-9]{2})([[:space:]]|$) ]]; then extracted="${BASH_REMATCH[1]}" current_tag="$(normalize "$extracted")" continue fi if [[ -z "${line// }" ]] || [[ "$line" =~ ^[[:space:]]*# ]]; then continue fi if selector_matches_tag "$selector" "$current_tag"; then EVENT_POOL_TAGS+=("$current_tag") EVENT_POOL_LINES+=("$line") fi done < "$file" done fi if [ "$GUARANTEE_HITS" = "1" ]; then local guaranteed_tags=( "a1-01" "a1-02" "a2-02" "a2-03" "a2-05" "a2-10" "c1-01" "c1-01" ) local guaranteed_lines=( "soc_event=dns_ioc event_type=ioc_dns_traffic src_ip=10.26.45.214 query=ioc-2294.malicious.example action=blocked severity=medium" "soc_event=dns_ioc event_type=ioc_domain_match src_ip=10.26.45.214 query=bad-c2.example feed=internal_main confidence=high action=alert" "date=2026-03-09 time=10:02:04 devname=\"FGT80F-Branch01\" devid=\"FGT80FTK20000001\" eventtime=1773079324 vd=\"root\" logid=\"0100044547\" type=\"event\" subtype=\"system\" level=\"warning\" user=\"admin\" action=\"password-change\" ui=\"https(10.20.55.1)\"" "date=2026-03-09 time=10:02:17 devname=\"FGT80F-Branch01\" devid=\"FGT80FTK20000001\" eventtime=1773079337 vd=\"root\" logid=\"0100044548\" type=\"event\" subtype=\"system\" level=\"warning\" user=\"admin\" action=\"create-admin\" target_user=\"soc-backup-admin\"" "date=2026-03-09 time=10:04:03 devname=\"FGT80F-Branch01\" devid=\"FGT80FTK20000001\" eventtime=1773079443 vd=\"root\" logid=\"0100044552\" type=\"event\" subtype=\"system\" level=\"notice\" user=\"admin\" action=\"download-config\" dstip=10.20.50.33" "date=2026-03-09 time=10:07:59 devname=\"FGT80F-Branch01\" devid=\"FGT80FTK20000001\" eventtime=1773079679 vd=\"root\" logid=\"0000000014\" type=\"traffic\" subtype=\"forward\" level=\"warning\" srcip=10.20.55.50 dstip=203.0.113.60 dstport=443 threat_label=\"known-c2\" action=\"accept\"" "date=2026-03-09 time=10:31:00 devname=\"FGT80F-Branch01\" devid=\"FGT80FTK20000001\" eventtime=1773081060 vd=\"root\" logid=\"0101037135\" type=\"event\" subtype=\"vpn\" tunneltype=\"ssl\" action=\"ssl-login-success\" user=\"analyst01\" srcip=203.0.113.71 previous_country=TH current_country=US" "soc_event=correlation event_type=c1_impossible_travel user=\"analyst01\" src_ip=203.0.113.71 prev_ip=203.0.113.11 prev_country=TH current_country=US distance_km=13890 travel_minutes=18" ) for idx in "${!guaranteed_lines[@]}"; do tag="${guaranteed_tags[$idx]}" if selector_matches_tag "$selector" "$tag"; then EVENT_POOL_TAGS+=("$tag") EVENT_POOL_LINES+=("${guaranteed_lines[$idx]}") fi done fi EVENT_POOL_READY=1 } send_random_event_once() { local selector size idx line tag selector="$1" if [ "$EVENT_POOL_READY" -ne 1 ] || [ "$EVENT_POOL_SELECTOR" != "$selector" ]; then build_event_pool "$selector" fi size="${#EVENT_POOL_LINES[@]}" if [ "$size" -eq 0 ]; then echo "random_sent=0 selector=$(normalize "$selector")" return 0 fi idx=$((RANDOM % size)) tag="${EVENT_POOL_TAGS[$idx]}" line="${EVENT_POOL_LINES[$idx]}" line="$(mock_line "$line")" emit_syslog "$line" echo "random_sent=1 tag=${tag} selector=$(normalize "$selector")" } if ! is_valid_selector "$SELECTOR"; then echo "error: selector must be one of all|a|b|c|appendix-a|appendix-b|appendix-c|a1..a4|b1..b3|c1..c3|A1-01..C3-04" exit 1 fi FILES=() while IFS= read -r f; do [ -n "$f" ] && FILES+=("$f") done < <(sample_files_for_selector "$SELECTOR") for f in "${FILES[@]}"; do if [ ! -f "$f" ]; then echo "error: missing sample file '$f'" exit 1 fi done echo "selector=${SELECTOR} count=${COUNT} delay=${DELAY}s forever=${FOREVER} dry_run=${DRY_RUN} mock_values=${MOCK_VALUES} guarantee_hits=${GUARANTEE_HITS} random_types=${RANDOM_TYPES} include_nonalerts=${INCLUDE_NONALERTS} docker_send=${DOCKER_SEND} nc_wait=${NC_WAIT_SECONDS}s strict_send=${STRICT_SEND}" echo "target=${WAZUH_SYSLOG_HOST}:${WAZUH_SYSLOG_PORT}/udp" if [ "$RANDOM_TYPES" = "1" ]; then if [ "$FOREVER" -eq 1 ]; then loop=1 while true; do send_random_event_once "$SELECTOR" echo "loop=${loop} complete" loop=$((loop + 1)) sleep "$DELAY" done else for ((i=1; i<=COUNT; i++)); do send_random_event_once "$SELECTOR" echo "iteration=${i}/${COUNT} complete" sleep "$DELAY" done fi elif [ "$FOREVER" -eq 1 ]; then loop=1 while true; do for f in "${FILES[@]}"; do send_file_once "$f" "$SELECTOR" done if [ "$GUARANTEE_HITS" = "1" ]; then send_guaranteed_hits_once "$SELECTOR" fi echo "loop=${loop} complete" loop=$((loop + 1)) done else for ((i=1; i<=COUNT; i++)); do for f in "${FILES[@]}"; do send_file_once "$f" "$SELECTOR" done if [ "$GUARANTEE_HITS" = "1" ]; then send_guaranteed_hits_once "$SELECTOR" fi echo "iteration=${i}/${COUNT} complete" done fi