#!/usr/bin/env bash
# Barracuda CVE-2026-116606 / CVE-2026-111560 offline firmware checker.
# STRICTLY READ-ONLY AND OFFLINE: compares version strings you supply (or a CSV
# inventory / exported syslog file you supply) against published data. It never
# contacts an appliance, never touches the admin interface or timezone setting,
# sends no network traffic and writes no files.
# Version data (checked 3 Nov 2026):
#   Barracuda Trust Center + Campus ESG article: ESG fixed in 9.4.0.029.
#   CVE.org / NVD CNA records: CVE-2026-111560 affects ESG <=9.4.0.27, ADC <=6.4.0,
#   WAF <=11.0.1, WSG <=16.1.100.100, BMA <=5.5.4.0, WCC <=2.2.1; CVE-2026-116606
#   affects ESG <9.4.0.030. Barracuda publishes NO fixed firmware number for
#   ADC/WAF/WSG/BMA/WCC (fix is a patch: BNADC-16586, BNYF-24789, BNMA-32468, BNWF-60060).
# Exit codes: 0 all PATCHED, 1 any LIKELY VULNERABLE, 2 any UNKNOWN (none vulnerable), 3 error.
set -u
KEV_STATUS="CISA KEV: CVE-2026-116606 and CVE-2026-111560 NOT listed (catalog 2026.10.30, checked 3 Nov 2026). No KEV due date. Re-check the feed."

usage() {
  cat <<'U'
Usage:
  check-barracuda.sh --product <ESG|ADC|WAF|WSG|BMA|WCC> --version <firmware>
  check-barracuda.sh --csv inventory.csv      (header: name,product,version)
  Optional with either: --syslog exported.log  (counts timezone changes / admin logins; keywords UNVERIFIED)
U
}

norm_product() { # free text -> code or empty
  local p
  p=$(printf '%s' "$1" | tr '[:upper:]' '[:lower:]')
  case "$p" in
    *wcc*|*"control center"*) echo WCC ;;
    esg|*"email security"*|*"spam firewall"*) echo ESG ;;
    adc|*"load balancer"*|*"application delivery"*) echo ADC ;;
    waf|*"web application firewall"*) echo WAF ;;
    wsg|*"web security gateway"*|*"web filter"*) echo WSG ;;
    bma|*"message archiver"*) echo BMA ;;
    *) echo "" ;;
  esac
}

# ver_cmp A B -> prints -1, 0 or 1 (numeric dotted compare, any length)
ver_cmp() {
  local -a a b; local i n x y
  IFS=. read -r -a a <<<"$1"; IFS=. read -r -a b <<<"$2"
  n=${#a[@]}; [ ${#b[@]} -gt "$n" ] && n=${#b[@]}
  for ((i=0; i<n; i++)); do
    x=${a[$i]:-0}; y=${b[$i]:-0}
    if ((10#$x > 10#$y)); then echo 1; return; fi
    if ((10#$x < 10#$y)); then echo -1; return; fi
  done
  echo 0
}

# verdict PRODUCT VERSION -> "VERDICT|reason"
verdict() {
  local prod v ceil
  prod=$(norm_product "$1"); v=$(printf '%s' "$2" | grep -oE '[0-9]+(\.[0-9]+)+' | head -n1)
  if [ -z "$prod" ]; then echo "UNKNOWN|product '$1' is not one of the six affected products"; return; fi
  if [ -z "$v" ]; then echo "UNKNOWN|no firmware version could be parsed from '$2'"; return; fi
  if [ "$prod" = ESG ]; then
    if [ "$(ver_cmp "$v" 9.4.0.029)" -ge 0 ]; then
      echo "PATCHED|ESG $v >= 9.4.0.029 (Barracuda fixed firmware). Note: CVE record for CVE-2026-116606 says affected below 9.4.0.030"; return; fi
    if [ "$(ver_cmp "$v" 9.1)" -lt 0 ]; then
      echo "LIKELY VULNERABLE|ESG $v < 9.4.0.029; older than 9.1 so update in TWO steps (cannot jump straight to 9.4.0.029)"; return; fi
    echo "LIKELY VULNERABLE|ESG $v < 9.4.0.029 (unless security patch BNSF-40425/BNSF-40430 is confirmed applied); upgrade via Advanced > Firmware Update"; return
  fi
  case "$prod" in
    ADC) ceil=6.4.0 ;; WAF) ceil=11.0.1 ;; WSG) ceil=16.1.100.100 ;; BMA) ceil=5.5.4.0 ;; WCC) ceil=2.2.1 ;;
  esac
  if [ "$(ver_cmp "$v" "$ceil")" -le 0 ]; then
    echo "LIKELY VULNERABLE|$prod $v <= $ceil (CVE-2026-111560 affected range); confirm Barracuda patch applied or update to latest firmware"
  else
    echo "UNKNOWN|$prod $v is above the published affected range (<= $ceil) but Barracuda publishes no fixed firmware number; confirm patch status with Barracuda"
  fi
}

worst=0
record() { case "$1" in "LIKELY VULNERABLE") worst=1 ;; UNKNOWN) [ "$worst" -eq 0 ] && worst=2 ;; esac; }
report() { # name product version
  local r; r=$(verdict "$2" "$3")
  printf '%s,%s,%s,%s,%s\n' "$1" "$2" "$3" "${r%%|*}" "${r#*|}"
  record "${r%%|*}"
}

product=""; version=""; csv=""; syslog=""
[ $# -ge 1 ] || { usage >&2; exit 3; }
while [ $# -gt 0 ]; do
  case "$1" in
    --product) product=${2-}; shift 2 || { usage >&2; exit 3; } ;;
    --version) version=${2-}; shift 2 || { usage >&2; exit 3; } ;;
    --csv) csv=${2-}; shift 2 || { usage >&2; exit 3; } ;;
    --syslog) syslog=${2-}; shift 2 || { usage >&2; exit 3; } ;;
    -h|--help) usage; exit 0 ;;
    *) echo "unknown option: $1" >&2; usage >&2; exit 3 ;;
  esac
done
if [ -z "$csv" ] && { [ -z "$product" ] || [ -z "$version" ]; }; then
  echo "error: give --product and --version, or --csv" >&2; usage >&2; exit 3; fi

echo "$KEV_STATUS"
echo "name,product,version,verdict,reason"
if [ -n "$csv" ]; then
  [ -r "$csv" ] || { echo "error: cannot read $csv" >&2; exit 3; }
  header=$(head -n1 "$csv" | tr -d '\r' | tr '[:upper:]' '[:lower:]')
  IFS=, read -r -a cols <<<"$header"
  ni=-1; pi=-1; vi=-1
  for i in "${!cols[@]}"; do
    c=$(printf '%s' "${cols[$i]}" | tr -d ' "')
    case "$c" in name) ni=$i ;; product) pi=$i ;; version) vi=$i ;; esac
  done
  if [ "$pi" -lt 0 ] || [ "$vi" -lt 0 ]; then echo "error: CSV needs 'product' and 'version' columns" >&2; exit 3; fi
  n=0
  while IFS=, read -r -a f; do
    [ ${#f[@]} -eq 0 ] && continue
    n=$((n+1))
    nm="row$n"; [ "$ni" -ge 0 ] && nm=$(printf '%s' "${f[$ni]:-}" | tr -d '"\r')
    report "$nm" "$(printf '%s' "${f[$pi]:-}" | tr -d '"\r')" "$(printf '%s' "${f[$vi]:-}" | tr -d '"\r')"
  done < <(tail -n +2 "$csv")
  [ "$n" -gt 0 ] || { echo "error: CSV has no data rows" >&2; exit 3; }
else
  report "cli" "$product" "$version"
fi

if [ -n "$syslog" ]; then
  [ -r "$syslog" ] || { echo "error: cannot read $syslog" >&2; exit 3; }
  # Keywords are UNVERIFIED: Barracuda has not published log strings for these CVEs.
  tz=$(grep -ciE 'time ?zone' "$syslog")
  lg=$(grep -iE 'admin' "$syslog" | grep -ciE 'log(ged)? ?in|login|authenticat')
  echo "syslog ($syslog): timezone-related lines=$tz admin-login lines=$lg (keywords UNVERIFIED; review lines manually, a count is not evidence of compromise)"
fi
exit "$worst"
