#!/usr/bin/env bash
# check-cve-2026-98432.sh — short exposure report for CVE-2026-98432
# (Linux IPv4 TCP use-after-free). Read-only: changes nothing.
# Prints a summary only — no IP addresses, no hostnames.
# Run:  sudo bash check-cve-2026-98432.sh   (works without sudo, some checks less complete)
# Exit codes:
#   0  PATCHED
#   1  EXPOSED (also: vulnerable kernel with low remote exposure)
#   2  FIX INSTALLED, NOT ACTIVE (reboot needed)
#   3  MITIGATED

CVE="CVE-2026-98432"
K="$(uname -r)"
. /etc/os-release 2>/dev/null
CODENAME="${VERSION_CODENAME:-unknown}"

echo "== $CVE check =="
echo "release        : ${VERSION_ID:-?} ($CODENAME)"
echo "running kernel : $K"

# 1. Does the RUNNING kernel's changelog mention the CVE? (Ubuntu lists CVEs in kernel changelogs)
has_fix() {  # $1 = kernel version string
  for p in linux-image-"$1" linux-modules-"$1" linux-image-unsigned-"$1"; do
    f="/usr/share/doc/$p/changelog.Debian.gz"
    [ -r "$f" ] && zgrep -q "$CVE" "$f" && return 0
  done
  return 1
}
if has_fix "$K"; then RUN_FIXED=yes; else RUN_FIXED=no; fi
echo "running kernel has fix (changelog): $RUN_FIXED"

# 2. Installed but not running kernels that contain the fix (=> reboot needed)
PENDING=""
for d in /usr/share/doc/linux-modules-[0-9]*; do
  v="${d#/usr/share/doc/linux-modules-}"
  [ "$v" = "$K" ] && continue
  [ -d "$d" ] && has_fix "$v" && PENDING="$PENDING $v"
done
echo "installed fixed kernel not running:${PENDING:- none}"

# 3. Fallback: compare ABI against Ubuntu's announced fix versions (generic kernels only)
ABI_NOTE="n/a"
base="${K%%-*}"; rest="${K#*-}"; abi="${rest%%-*}"; flavor="${rest#*-}"
case "$CODENAME:$base" in
  jammy:5.15.0)   need=203 ;;
  noble:6.8.0)    need=149 ;;
  resolute:7.0.0) need=43  ;;
  stonking:7.3.0) need=10  ;;
  *)              need="" ;;
esac
if [ -n "$need" ] && [ "$flavor" = "generic" ]; then
  if [ "$abi" -ge "$need" ] 2>/dev/null; then ABI_NOTE="ABI $abi >= $need (Ubuntu tracker target, unconfirmed; the changelog line above decides)"
  else ABI_NOTE="ABI $abi < $need (below Ubuntu tracker target)"; fi
fi
echo "ABI vs announced fix  : $ABI_NOTE"

# 4. Ubuntu Pro client's own verdict, if available
if command -v pro >/dev/null 2>&1; then
  PRO="$(pro fix "$CVE" --dry-run 2>&1 | grep -iE "$CVE.*(resolved|affected|not found|fix)" | tail -n1)"
  echo "pro fix verdict       : ${PRO:-no clear answer}"
else
  echo "pro fix verdict       : pro client not installed"
fi

# 5. Livepatch
if command -v canonical-livepatch >/dev/null 2>&1; then
  LP="$(canonical-livepatch status 2>/dev/null | grep -m1 -iE 'running|state|enabled' | sed 's/^ *//')"
  echo "livepatch             : ${LP:-installed, status unknown (try sudo)}"
else
  echo "livepatch             : not installed"
fi

# 6. Attack-surface settings
MIG="$(sysctl -n net.ipv4.tcp_migrate_req 2>/dev/null || echo n/a)"
MPE="$(sysctl -n net.mptcp.enabled 2>/dev/null || echo n/a)"
MPL="$(ss -Hln -M 2>/dev/null | wc -l)"
echo "tcp_migrate_req       : $MIG   (1 = extra attack path)"
echo "mptcp enabled         : $MPE"
echo "mptcp listeners       : $MPL"

# 7. IPv4 TCP listeners not bound to loopback — ports only, no addresses
# If ss itself fails, report "unknown" instead of an empty list (which would read as low exposure).
if SS_OUT="$(ss -Hltn 2>/dev/null)"; then
  PORTS="$(printf '%s\n' "$SS_OUT" | awk '{print $4}' | grep -E '^(\*|[0-9]+(\.[0-9]+){3}(%[^:]*)?):' | grep -v '^127\.' | sed 's/.*://' | sort -un | tr '\n' ' ')"
  echo "non-loopback IPv4 TCP ports: ${PORTS:-none}"
else
  PORTS="unknown (ss failed)"
  echo "non-loopback IPv4 TCP ports: unknown (ss missing or failed)"
fi

# 8. Reboot flag
[ -f /var/run/reboot-required ] && RB=yes || RB=no
echo "reboot-required flag  : $RB"

# 9. Interim nft mitigation (table inet cve98432); nft list needs root
MIT_STATE=unknown; MIT_TXT="unknown (not root, firewall not readable)"
if [ "$(id -u)" = 0 ]; then
  if command -v nft >/dev/null 2>&1 && NFT="$(nft list table inet cve98432 2>/dev/null)"; then
    if echo "$NFT" | grep -q 'hook prerouting' && echo "$NFT" | grep -q 'hdrlength' && echo "$NFT" | grep -q 'drop'; then
      MIT_STATE=active
      MIT_TXT="active ($(echo "$NFT" | grep -o 'packets [0-9]*' | head -n1 | cut -d' ' -f2) packets dropped since load)"
    else
      MIT_STATE=broken; MIT_TXT="table present but drop rule not found"
    fi
  else
    MIT_STATE=absent; MIT_TXT="not present"
  fi
fi
echo "interim nft mitigation: $MIT_TXT"

# Verdict
echo "--"
if [ "$RUN_FIXED" = yes ]; then
  echo "VERDICT: PATCHED (running kernel contains the fix)"
elif [ -n "$PENDING" ]; then
  echo "VERDICT: FIX INSTALLED, NOT ACTIVE — reboot into:$PENDING"
elif [ "$MIT_STATE" = active ]; then
  echo "VERDICT: MITIGATED (interim) — kernel unpatched, nft drop rule active; still plan the kernel update"
elif [ -z "$PORTS" ]; then
  echo "VERDICT: VULNERABLE KERNEL, but no non-loopback IPv4 TCP listeners (low remote exposure)"
else
  echo "VERDICT: EXPOSED — unpatched kernel with IPv4 TCP ports open: $PORTS"
fi
[ "$MIT_STATE" = unknown ] && [ "$RUN_FIXED" != yes ] && echo "note: not root, so the interim nft mitigation could not be checked"

# Exit code (table in the header)
if [ "$RUN_FIXED" = yes ]; then
  exit 0
elif [ -n "$PENDING" ]; then
  exit 2
elif [ "$MIT_STATE" = active ]; then
  exit 3
else
  exit 1
fi
