#!/usr/bin/env bash # discover-fortigate.sh — pull DHCP lease list from a FortiGate. # # SSHes to a FortiGate admin account, runs `execute dhcp lease-list`, # parses the output into TSV (IP, MAC, hostname, vdom, source). # # Usage: # scripts/discover-fortigate.sh # # Env overrides: # FORTIGATE_SSH_USER default: admin # # Output: TSV on stdout, one lease per line: # IP MAC HOSTNAME VDOM SOURCE # Where SOURCE is "fortigate:" so multiple runs can be concatenated # and still identified. # # Example: # scripts/discover-fortigate.sh ana-fw.phasefinal.com > leases-ana.tsv # scripts/discover-fortigate.sh nh3-gw.phasefinal.com > leases-nh3.tsv # # Requires: ssh config (or `~/.ssh/config` host alias) for the FortiGate, # with key auth OR interactive password. FortiGate's SSH expects admin-level # credentials. set -euo pipefail if [ -z "${1:-}" ]; then echo "usage: $(basename "$0") " >&2 exit 2 fi HOST="$1" USER="${FORTIGATE_SSH_USER:-admin}" # FortiGate CLI command. `execute dhcp lease-list all` dumps every vdom. # If the device is single-vdom, `execute dhcp lease-list` (no arg) also works. # # Do NOT suppress stderr — FortiGate's error messages are the main debugging # signal when the command returns empty. Let them flow to the caller. raw="" for cmd in 'execute dhcp lease-list all' 'execute dhcp lease-list'; do raw=$(ssh -o StrictHostKeyChecking=accept-new "${USER}@${HOST}" "$cmd" || true) if [ -n "$raw" ] && ! grep -qiE 'unknown action|parse error|command fail' <<<"$raw"; then break fi done if [ -z "$raw" ]; then echo "error: no lease data from $HOST" >&2 echo " — try running 'ssh ${USER}@${HOST}' interactively and running" >&2 echo " 'execute dhcp lease-list ?' to discover the right subcommand shape" >&2 exit 1 fi # Parse. # # FortiGate lease output looks approximately like: # # VDOM: root # Interface: internal # IP MAC Hostname Lease-Expiry # 10.250.50.100 00:1a:2b:3c:4d:5e laptop-01 Mon Apr 21 14:30:00 2026 # 10.250.50.101 aa:bb:cc:dd:ee:ff printer Mon Apr 21 15:00:00 2026 # # Format varies by FortiOS version. The awk below is defensive: # - only accepts lines where field 1 matches IPv4 # - assumes MAC is field 2, hostname is field 3-4 (may contain whitespace), # which is the most common layout. awk -v src="fortigate:${HOST}" -v vdom="" ' /^VDOM:/ { vdom=$2; next } /^Interface:/ { next } /^IP[[:space:]]/ { next } /^[[:space:]]*$/ { next } $1 ~ /^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$/ { ip = $1 mac = $2 # Hostname may be missing (shows "-" or empty) or contain spaces; take # the rest of the line minus the trailing date fields. host = "" for (i=3; i<=NF-6; i++) host = host (host=="" ? "" : " ") $i if (host == "" || host == "-") host = "-" printf "%s\t%s\t%s\t%s\t%s\n", ip, mac, host, (vdom==""?"-":vdom), src } ' <<<"$raw"