#!/usr/bin/env bash # discover-gaps.sh — find devices in discovery TSVs that aren't in # servers/*/ssh-target (the authoritative "managed hosts" list). # # Input: one or more TSV files, each a line of: # IP MAC HOSTNAME (EXTRA1) (EXTRA2) … SOURCE # Produced by discover-fortigate.sh, discover-unifi.sh, or equivalent. # # Usage: # scripts/discover-gaps.sh [--ignore-unifi] leases.tsv [more.tsv ...] # # Flags: # --ignore-unifi Drop rows whose final column starts with "unifi:". # Useful when you want to focus on servers / IoT / # endpoints rather than UniFi infrastructure # (APs, switches, UDMs). # # An IP is considered "known" if it matches any of: # - an IPv4 in servers//ssh-target # - a resolvable hostname in ssh-target # - any IPv4 token found in servers//README.md # - any IPv4 token in servers//proxmox-details.txt # (so Proxmox-tracked VM IPs surfaced by proxmox_inspect.sh are # automatically absorbed) # # Exit code: # 0 if zero gaps found # 1 if one or more unmanaged IPs detected (useful for cron alerting) set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" SERVERS_DIR="$REPO_ROOT/servers" IGNORE_UNIFI=0 FILES=() for arg in "$@"; do case "$arg" in --ignore-unifi) IGNORE_UNIFI=1 ;; -h|--help) sed -n '2,30p' "$0" exit 0 ;; -*) echo "error: unknown flag $arg" >&2 exit 2 ;; *) FILES+=("$arg") ;; esac done if [ "${#FILES[@]}" -lt 1 ]; then echo "usage: $(basename "$0") [--ignore-unifi] [more.tsv ...]" >&2 exit 2 fi # ---- Build the set of managed IPs --------------------------------------- # 1. servers/*/ssh-target managed_ips=$( for stf in "$SERVERS_DIR"/*/ssh-target; do [ -r "$stf" ] || continue target=$(awk 'NF{print $1; exit}' "$stf") val=${target##*@} if [[ "$val" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]]; then echo "$val" else ip=$(getent hosts "$val" 2>/dev/null | awk 'NR==1{print $1}') [ -n "$ip" ] && echo "$ip" fi done | sort -u ) if [ -z "$managed_ips" ]; then echo "warning: no managed IPs found under $SERVERS_DIR/*/ssh-target" >&2 fi # 2. Any IPv4 in README.md files under servers/ readme_ips=$( grep -rhEo '([0-9]+\.){3}[0-9]+' "$SERVERS_DIR"/*/README.md 2>/dev/null \ | sort -u || true ) # 3. Any IPv4 in proxmox-details.txt (VM IPs + cluster-resources table) pve_ips=$( grep -rhEo '([0-9]+\.){3}[0-9]+' "$SERVERS_DIR"/*/proxmox-details.txt 2>/dev/null \ | sort -u || true ) # 4. Any IPv4 in system-details.txt (ip addr show output, docker inspect, etc.) sys_ips=$( grep -rhEo '([0-9]+\.){3}[0-9]+' "$SERVERS_DIR"/*/system-details.txt 2>/dev/null \ | sort -u || true ) # Drop meaningless addresses from the "known" set (they'd falsely mask # real unmanaged hosts that happen to also be 127.0.0.1 in some probe). all_known=$( printf '%s\n%s\n%s\n%s\n' "$managed_ips" "$readme_ips" "$pve_ips" "$sys_ips" \ | grep -Ev '^(0\.0\.0\.0|127\.|255\.255\.255\.255|169\.254\.)' \ | sort -u ) # ---- Filter discovery TSVs --------------------------------------------- unmanaged=$( cat "${FILES[@]}" | awk -F'\t' -v known="$all_known" -v drop_unifi="$IGNORE_UNIFI" ' BEGIN { n = split(known, arr, "\n") for (i=1; i<=n; i++) if (arr[i] != "") k[arr[i]] = 1 } # Need a valid IP in column 1 and IP must not be "known". NF >= 1 && $1 ~ /^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$/ && !($1 in k) { if (drop_unifi && $NF ~ /^unifi:/) next print } ' | sort -t$'\t' -k1,1 -V -u ) if [ -z "$unmanaged" ]; then echo "no unmanaged devices — every discovered IP is tracked under servers/" >&2 exit 0 fi printf '%s\n' "$unmanaged" exit 1