| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551 |
- #!/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
|