#!/usr/bin/env bash
#
# check_ssh_access.sh
#
# Verifies that SSH (port 22) on a list of VMs is reachable from this machine.
# Run it FROM the dedicated line whose IP is supposed to be allowlisted.
#
# The VM IPs are read from list.txt (same folder as this script), one per line:
#
#   192.168.0.202
#   192.168.0.203
#   192.168.0.204
#   192.168.0.205
#
# Blank lines and lines starting with '#' are ignored.
#
# Usage:
#   ./check_ssh_access.sh                  # uses ./list.txt
#   ./check_ssh_access.sh other.txt        # uses a different file
#   ./check_ssh_access.sh -p 2222          # different port
#   ./check_ssh_access.sh -t 8             # longer timeout
#   ./check_ssh_access.sh -n               # skip the public-IP check
#   ./check_ssh_access.sh -c > report.csv  # CSV output

set -uo pipefail

# ---- your dedicated IPs -----------------------------------------------------
ALLOWED_IPS=("72.255.34.20" "103.83.89.157")

# ---- defaults ---------------------------------------------------------------
LIST_FILE="$(dirname "$0")/list.txt"
PORT=22
TIMEOUT=5
BANNER_WAIT=3
SKIP_IP_CHECK=0
CSV=0

usage() { sed -n '3,26p' "$0" | sed 's/^# \{0,1\}//'; exit 0; }

while getopts ":p:t:nch" opt; do
  case "$opt" in
    p) PORT="$OPTARG" ;;
    t) TIMEOUT="$OPTARG" ;;
    n) SKIP_IP_CHECK=1 ;;
    c) CSV=1 ;;
    h) usage ;;
    *) echo "unknown option -$OPTARG" >&2; exit 2 ;;
  esac
done
shift $((OPTIND - 1))

[ $# -ge 1 ] && LIST_FILE="$1"

if [ ! -f "$LIST_FILE" ]; then
  echo "error: list file not found: $LIST_FILE" >&2
  echo "       create it with one VM IP per line." >&2
  exit 2
fi

# ---- colours (only on a terminal) -------------------------------------------
if [ -t 1 ] && [ "$CSV" -eq 0 ]; then
  G=$'\e[32m'; R=$'\e[31m'; Y=$'\e[33m'; B=$'\e[1m'; N=$'\e[0m'
else
  G=""; R=""; Y=""; B=""; N=""
fi

# ---- read the list ----------------------------------------------------------
TARGETS=()
while IFS= read -r line || [ -n "$line" ]; do
  line="${line%%#*}"
  line="${line//$'\r'/}"
  line="$(echo "$line" | xargs)"
  [ -z "$line" ] && continue
  TARGETS+=("$line")
done < "$LIST_FILE"

if [ ${#TARGETS[@]} -eq 0 ]; then
  echo "error: no IPs found in $LIST_FILE" >&2
  exit 2
fi

# ---- confirm where we are coming from ---------------------------------------
if [ "$SKIP_IP_CHECK" -eq 0 ]; then
  MY_IP=""
  for svc in "https://api.ipify.org" "https://ifconfig.me/ip" "https://icanhazip.com"; do
    MY_IP="$(curl -4 -fsS --max-time 5 "$svc" 2>/dev/null | tr -d '[:space:]')"
    [ -n "$MY_IP" ] && break
  done

  if [ -z "$MY_IP" ]; then
    echo "${Y}warning:${N} could not determine this machine's public IP" >&2
  else
    match=0
    for ip in "${ALLOWED_IPS[@]}"; do
      [ "$MY_IP" = "$ip" ] && match=1
    done
    if [ "$match" -eq 1 ]; then
      [ "$CSV" -eq 0 ] && echo "${B}Source IP:${N} $MY_IP ${G}(dedicated — results are valid)${N}"
    else
      echo "${R}WARNING:${N} your public IP is $MY_IP, which is NOT one of your dedicated IPs" >&2
      echo "         (${ALLOWED_IPS[*]}). Results will not reflect your allowlist." >&2
    fi
  fi
fi

# ---- the probe --------------------------------------------------------------
# Returns:  0 = open (banner on stdout)   1 = refused   2 = filtered/timeout
#           3 = DNS / host resolution failure
probe() {
  local host="$1" out rc

  out="$(timeout "$TIMEOUT" bash -c '
      exec 3<>"/dev/tcp/$0/$1" 2>/dev/null || exit 1
      IFS= read -r -t '"$BANNER_WAIT"' banner <&3
      printf "%s" "$banner"
      exec 3<&-
    ' "$host" "$PORT" 2>/dev/null)"
  rc=$?

  case $rc in
    0)   printf '%s' "$out"; return 0 ;;
    124) return 2 ;;
    *)   getent hosts "$host" >/dev/null 2>&1 || return 3
         return 1 ;;
  esac
}

# ---- run --------------------------------------------------------------------
ok=0; blocked=0; refused=0; bad=0

if [ "$CSV" -eq 1 ]; then
  echo "host,port,status,detail"
else
  printf '%-20s %-11s %s\n' "VM" "PORT $PORT" "DETAIL"
  printf '%s\n' "---------------------------------------------------------------------------"
fi

for host in "${TARGETS[@]}"; do
  banner="$(probe "$host")"; rc=$?

  case $rc in
    0)
      ok=$((ok+1))
      status="ALLOWED"; colour="$G"
      if [ -n "$banner" ]; then
        detail="$(echo "$banner" | tr -d '\r')"
      else
        detail="port open (no banner)"
      fi
      ;;
    1)
      refused=$((refused+1))
      status="REFUSED"; colour="$Y"
      detail="reachable, but rejected — sshd down, or a REJECT rule"
      ;;
    2)
      blocked=$((blocked+1))
      status="BLOCKED"; colour="$R"
      detail="no reply in ${TIMEOUT}s — dropped by firewall, or host down"
      ;;
    3)
      bad=$((bad+1))
      status="NO-HOST"; colour="$R"
      detail="cannot resolve hostname"
      ;;
  esac

  if [ "$CSV" -eq 1 ]; then
    printf '%s,%s,%s,"%s"\n' "$host" "$PORT" "$status" "${detail//\"/\"\"}"
  else
    printf '%-20s %s%-11s%s %s\n' "$host" "$colour" "$status" "$N" "$detail"
  fi
done

if [ "$CSV" -eq 0 ]; then
  printf '%s\n' "---------------------------------------------------------------------------"
  printf '%s allowed  |  %s blocked  |  %s refused  |  %s unresolved   (total %s)\n' \
    "$ok" "$blocked" "$refused" "$bad" "${#TARGETS[@]}"
fi

[ "$ok" -eq "${#TARGETS[@]}" ] && exit 0
exit 1
