#!/usr/bin/env bash set -euo pipefail PROFILE="${1:-mixed}" MODELS="${2:-all}" BASE_DELAY="${3:-0.8}" WAZUH_SYSLOG_HOST="${WAZUH_SYSLOG_HOST:-127.0.0.1}" WAZUH_SYSLOG_PORT="${WAZUH_SYSLOG_PORT:-514}" SIM_MAX_EVENTS="${SIM_MAX_EVENTS:-0}" # 0 = run forever SIM_SRC_PREFIX="${SIM_SRC_PREFIX:-10.10.20}" SIM_VPN_USER="${SIM_VPN_USER:-remote.user}" SIM_ADMIN_USER="${SIM_ADMIN_USER:-admin}" EVENT_COUNT=0 if ! [[ "${BASE_DELAY}" =~ ^[0-9]+([.][0-9]+)?$ ]]; then echo "error: base delay must be numeric (e.g. 0.8)" exit 1 fi if ! [[ "${SIM_MAX_EVENTS}" =~ ^[0-9]+$ ]]; then echo "error: SIM_MAX_EVENTS must be an integer >= 0" exit 1 fi emit_syslog() { local msg="$1" local sent="false" if command -v nc >/dev/null 2>&1; then if printf "%s\n" "${msg}" | nc -u -w1 "${WAZUH_SYSLOG_HOST}" "${WAZUH_SYSLOG_PORT}"; then sent="true" fi fi if [[ "${sent}" != "true" ]]; then if printf "%s\n" "${msg}" >"/dev/udp/${WAZUH_SYSLOG_HOST}/${WAZUH_SYSLOG_PORT}" 2>/dev/null; then sent="true" fi fi if [[ "${sent}" != "true" ]]; then echo "error: failed to send syslog event to ${WAZUH_SYSLOG_HOST}:${WAZUH_SYSLOG_PORT}/udp" return 1 fi EVENT_COUNT=$((EVENT_COUNT + 1)) echo "[$(date -u +'%Y-%m-%dT%H:%M:%SZ')] sent(${EVENT_COUNT}): ${msg}" } pick_model() { case "${MODELS}" in 501E|501e) echo "501E" ;; 80F|80f) echo "80F" ;; 60F|60f) echo "60F" ;; 40F|40f) echo "40F" ;; all) case $((RANDOM % 4)) in 0) echo "501E" ;; 1) echo "80F" ;; 2) echo "60F" ;; *) echo "40F" ;; esac ;; *) echo "error: unknown models '${MODELS}'" echo "valid: 501E | 80F | 60F | 40F | all" exit 1 ;; esac } devname_for_model() { case "$1" in 501E) echo "FGT501E-HQ" ;; 80F) echo "FGT80F-Branch01" ;; 60F) echo "FGT60F-Store12" ;; 40F) echo "FGT40F-SMB01" ;; *) echo "FGT-UNKNOWN" ;; esac } devid_for_model() { case "$1" in 501E) echo "FGT501E12345678" ;; 80F) echo "FGT80FTK20000001" ;; 60F) echo "FGT60FTK21001234" ;; 40F) echo "FGT40FTK22004567" ;; *) echo "FGT00000000000000" ;; esac } src_ip() { echo "${SIM_SRC_PREFIX}.$((RANDOM % 220 + 10))" } event_ts() { echo "$(date +%s)" } syslog_prefix() { local pri="$1" local model="$2" local devname local devid devname="$(devname_for_model "${model}")" devid="$(devid_for_model "${model}")" printf "<%s>date=%s time=%s devname=\"%s\" devid=\"%s\" eventtime=%s vd=\"root\"" \ "${pri}" "$(date '+%Y-%m-%d')" "$(date '+%H:%M:%S')" "${devname}" "${devid}" "$(event_ts)" } send_traffic_allow() { local model="$1" local sip sip="$(src_ip)" emit_syslog "$(syslog_prefix 189 "${model}") logid=\"0000000013\" type=\"traffic\" subtype=\"forward\" level=\"notice\" srcip=${sip} srcport=$((RANDOM % 40000 + 1024)) srcintf=\"lan\" dstip=172.217.14.$((RANDOM % 200 + 20)) dstport=443 dstintf=\"wan1\" proto=6 service=\"HTTPS\" policyid=12 action=\"accept\" sentbyte=$((RANDOM % 4000 + 500)) rcvdbyte=$((RANDOM % 20000 + 1000)) duration=$((RANDOM % 60 + 2)) soc_mvp_test=true vendor=fortinet product=fortigate model=${model} event_type=fortigate_traffic_allow severity=low" } send_webfilter_block() { local model="$1" local sip sip="$(src_ip)" emit_syslog "$(syslog_prefix 190 "${model}") logid=\"0419016384\" type=\"utm\" subtype=\"webfilter\" level=\"warning\" srcip=${sip} srcintf=\"internal\" dstip=104.18.$((RANDOM % 200 + 1)).$((RANDOM % 240 + 10)) dstport=80 hostname=\"suspicious-$((RANDOM % 9000 + 1000)).example\" url=\"/dropper\" action=\"blocked\" cat=52 profile=\"WF-Strict\" policyid=3 soc_mvp_test=true vendor=fortinet product=fortigate model=${model} event_type=fortigate_webfilter_block severity=medium" } send_ips_block() { local model="$1" local sip sip="$(src_ip)" emit_syslog "$(syslog_prefix 190 "${model}") logid=\"0720018432\" type=\"utm\" subtype=\"ips\" level=\"alert\" srcip=${sip} dstip=45.155.$((RANDOM % 200 + 1)).$((RANDOM % 240 + 10)) srcport=$((RANDOM % 40000 + 1024)) dstport=445 sessionid=$((RANDOM % 9000000 + 1000000)) action=\"blocked\" attack=\"MS.SMB.Server.Service.Remote.Code.Execution\" severity=\"critical\" policyid=8 soc_mvp_test=true vendor=fortinet product=fortigate model=${model} event_type=fortigate_ips_block severity=high" } send_vpn_down() { local model="$1" emit_syslog "$(syslog_prefix 191 "${model}") logid=\"0104044546\" type=\"event\" subtype=\"vpn\" level=\"warning\" user=\"${SIM_VPN_USER}\" tunneltype=\"ipsec\" remip=203.0.113.$((RANDOM % 200 + 10)) locip=198.51.100.$((RANDOM % 200 + 10)) action=\"tunnel-down\" reason=\"peer-not-responding\" msg=\"IPsec tunnel Branch-HQ is down\" soc_mvp_test=true vendor=fortinet product=fortigate model=${model} event_type=fortigate_vpn_down severity=high" } send_admin_login() { local model="$1" emit_syslog "$(syslog_prefix 191 "${model}") logid=\"0100032002\" type=\"event\" subtype=\"system\" level=\"information\" logdesc=\"Admin login successful\" user=\"${SIM_ADMIN_USER}\" ui=\"https(10.20.1.$((RANDOM % 200 + 10)))\" msg=\"Administrator ${SIM_ADMIN_USER} logged in successfully\" soc_mvp_test=true vendor=fortinet product=fortigate model=${model} event_type=fortigate_admin_login severity=low" } send_event_by_profile() { local model="$1" local roll roll=$((RANDOM % 100)) case "${PROFILE}" in normal) if [[ "${roll}" -lt 80 ]]; then send_traffic_allow "${model}" elif [[ "${roll}" -lt 92 ]]; then send_admin_login "${model}" elif [[ "${roll}" -lt 98 ]]; then send_webfilter_block "${model}" else send_vpn_down "${model}" fi ;; incident) if [[ "${roll}" -lt 45 ]]; then send_traffic_allow "${model}" elif [[ "${roll}" -lt 70 ]]; then send_webfilter_block "${model}" elif [[ "${roll}" -lt 92 ]]; then send_ips_block "${model}" else send_vpn_down "${model}" fi ;; mixed) if [[ "${roll}" -lt 65 ]]; then send_traffic_allow "${model}" elif [[ "${roll}" -lt 82 ]]; then send_webfilter_block "${model}" elif [[ "${roll}" -lt 94 ]]; then send_admin_login "${model}" elif [[ "${roll}" -lt 98 ]]; then send_vpn_down "${model}" else send_ips_block "${model}" fi ;; *) echo "error: unknown profile '${PROFILE}'" echo "valid: normal | incident | mixed" exit 1 ;; esac } echo "starting FortiGate continuous simulator" echo "profile=${PROFILE} models=${MODELS} base_delay=${BASE_DELAY}s target=${WAZUH_SYSLOG_HOST}:${WAZUH_SYSLOG_PORT}/udp max_events=${SIM_MAX_EVENTS}" echo "press Ctrl+C to stop" trap 'echo; echo "stopped after ${EVENT_COUNT} events"; exit 0' INT TERM while true; do send_event_by_profile "$(pick_model)" if [[ "${SIM_MAX_EVENTS}" -gt 0 && "${EVENT_COUNT}" -ge "${SIM_MAX_EVENTS}" ]]; then echo "done: reached SIM_MAX_EVENTS=${SIM_MAX_EVENTS}" exit 0 fi # occasional burst to mimic short real-world spikes if [[ $((RANDOM % 100)) -lt 12 ]]; then send_event_by_profile "$(pick_model)" [[ "${SIM_MAX_EVENTS}" -gt 0 && "${EVENT_COUNT}" -ge "${SIM_MAX_EVENTS}" ]] && exit 0 send_event_by_profile "$(pick_model)" [[ "${SIM_MAX_EVENTS}" -gt 0 && "${EVENT_COUNT}" -ge "${SIM_MAX_EVENTS}" ]] && exit 0 fi sleep "${BASE_DELAY}" done