#!/usr/bin/env bash
# check-chrome.sh: strictly read-only, offline check of saved browser version data for Chrome CVE-2026-125350
# (use after free in XML, High per Google, exploited in the wild per Google, fixed 9 Nov 2026; in CISA KEV since
# 10 Nov 2026, due 24 Nov 2026 for US federal agencies only, patch now regardless).
# Reads only the values and files you pass. No network, no writes, no payloads, never starts a browser.
#
# Usage:
#   check-chrome.sh --version VER [--channel CH] [--browser NAME] [--platform OS] [--running VER]
#   check-chrome.sh --inventory FILE [--processes FILE]
# --version    one installed version string, for example 157.0.8103.50
# --channel    stable (default), extended, beta, dev or canary
# --browser    chrome (default), edge, brave, opera, vivaldi, chromium or another name
# --platform   windows, mac, linux or android (optional, changes the fixed build named in messages)
# --running    version of a browser process still running (saved from Task Manager, ps or an EDR export)
# --inventory  CSV header: host,browser,channel,version (for example from an RMM or Intune export)
# --processes  CSV header: host,process,version, one row per running browser process you saved
# Fixed builds (Google, 9 Nov 2026): Stable 157.0.8103.50 on all platforms (Windows and Mac also ship .51),
#   Extended Stable 156.0.8078.96 (Windows and Mac), Android 157.0.8103.50.
# Exit: 0 PATCHED, 1 VULNERABLE, 2 NEEDS RELAUNCH, 3 UNKNOWN, 4 usage or read error.
#       Inventory exits with the worst row (1 > 2 > 3 > 0).
set -u
export LC_ALL=C

CHECKED='10 Nov 2026 19:09 GMT'

usage() {
  cat >&2 <<'USAGE'
Usage: check-chrome.sh --version VER [--channel CH] [--browser NAME] [--platform OS] [--running VER]
       check-chrome.sh --inventory FILE [--processes FILE]
  --version    installed version, for example 157.0.8103.50
  --channel    stable (default), extended, beta, dev or canary
  --browser    chrome (default), edge, brave, opera, vivaldi, chromium or another name
  --platform   windows, mac, linux or android
  --running    version of a browser process that is still running
  --inventory  CSV: host,browser,channel,version
  --processes  CSV: host,process,version (needs --inventory)
Exit: 0 PATCHED, 1 VULNERABLE, 2 NEEDS RELAUNCH, 3 UNKNOWN, 4 usage or read error
USAGE
  exit 4
}
die() { echo "ERROR: $*" >&2; exit 4; }

lc() { printf '%s' "$1" | tr '[:upper:]' '[:lower:]'; }
trim() { local s=$1; s=${s#"${s%%[![:space:]]*}"}; s=${s%"${s##*[![:space:]]}"}; printf '%s' "$s"; }
valid_ver() { [[ $1 =~ ^[0-9]{1,6}\.[0-9]{1,6}\.[0-9]{1,6}\.[0-9]{1,6}$ ]]; }

# fix_state VER: 0 if the build has the fix, 1 if it does not, 2 if the number is not a Chrome release we can judge.
# 157.0.8103.50 and later 157.0.8103.x, 156.0.8078.96 and later 156.0.8078.x, and 158 to 199 count as fixed.
fix_state() {
  local a b c d
  IFS=. read -r a b c d <<< "$1"
  a=$((10#$a)); b=$((10#$b)); c=$((10#$c)); d=$((10#$d))
  if [ "$a" -ge 200 ] || [ "$b" -ne 0 ]; then return 2; fi
  if [ "$a" -gt 157 ]; then return 0; fi
  if [ "$a" -eq 157 ]; then
    if [ "$c" -gt 8103 ]; then return 2; fi
    if [ "$c" -eq 8103 ] && [ "$d" -ge 50 ]; then return 0; fi
    return 1
  fi
  if [ "$a" -eq 156 ]; then
    if [ "$c" -gt 8078 ]; then return 2; fi
    if [ "$c" -eq 8078 ] && [ "$d" -ge 96 ]; then return 0; fi
    return 1
  fi
  return 1
}
# ver_lt A B: 0 if A is lower than B (both valid)
ver_lt() {
  local -a x y; local i
  IFS=. read -r -a x <<< "$1"; IFS=. read -r -a y <<< "$2"
  for i in 0 1 2 3; do
    if [ "$((10#${x[i]}))" -lt "$((10#${y[i]}))" ]; then return 0; fi
    if [ "$((10#${x[i]}))" -gt "$((10#${y[i]}))" ]; then return 1; fi
  done
  return 1
}

norm_browser() {
  case $(lc "$(trim "$1")") in
    chrome|'google chrome'|chrome.exe|googlechrome|'google chrome helper'*) echo chrome;;
    edge|'microsoft edge'|msedge|msedge.exe) echo edge;;
    '') echo '';;
    *) lc "$(trim "$1")";;
  esac
}
norm_channel() {
  case $(lc "$(trim "$1")") in
    ''|stable) echo stable;;
    extended|'extended stable'|extended_stable|extended-stable|extendedstable) echo extended;;
    *) lc "$(trim "$1")";;
  esac
}

stable_fix() {
  case $1 in
    windows|mac) echo '157.0.8103.50 (Windows and Mac also ship 157.0.8103.51)';;
    linux|android) echo '157.0.8103.50';;
    *) echo '157.0.8103.50 (all platforms; Windows and Mac also ship 157.0.8103.51)';;
  esac
}

# RUNNING: newline list of "host|browser|version" from --processes, or one row for --version mode
RUNNING=""
RANK_PATCHED=0 RANK_UNKNOWN=1 RANK_RELAUNCH=2 RANK_VULN=3
worst=-1; n=0; c0=0; c1=0; c2=0; c3=0
record() { # rc
  local r
  case $1 in 0) r=$RANK_PATCHED; c0=$((c0+1));; 1) r=$RANK_VULN; c1=$((c1+1));; 2) r=$RANK_RELAUNCH; c2=$((c2+1));; *) r=$RANK_UNKNOWN; c3=$((c3+1));; esac
  n=$((n+1)); if [ "$r" -gt "$worst" ]; then worst=$r; fi
}

# lowest_running HOST BROWSER -> lowest valid running version saved for that host and browser
lowest_running() {
  local h b v low="" line
  while IFS= read -r line; do
    [ -n "$line" ] || continue
    IFS='|' read -r h b v <<< "$line"
    if [ "$h" != "$1" ] || [ "$b" != "$2" ]; then continue; fi
    if [ -z "$low" ] || ver_lt "$v" "$low"; then low=$v; fi
  done <<< "$RUNNING"
  printf '%s' "$low"
}

judge() { # host browser channel version platform
  local host=$1 br=$2 ch=$3 ver=$4 plat=$5 where low st
  where="$host (${br:-browser not given}, $ch, ${ver:-no version})"
  if [ "$br" != chrome ]; then
    if [ "$br" = edge ]; then
      echo "UNKNOWN: $where: Microsoft had not released a fixed Edge build when this pack was checked ($CHECKED). Its security release notes say it is \"actively working on releasing a security fix\". Check them before you mark Edge patched."
    elif [ -z "$br" ]; then
      echo "UNKNOWN: $where: browser not given."
    else
      echo "UNKNOWN: $where: this pack does not judge this browser's versions. Check its vendor's release notes for the Chromium fix for CVE-2026-125350 (the README lists the Brave and Opera notes seen at $CHECKED)."
    fi
    record 3; return
  fi
  case $ch in
    stable|extended) ;;
    beta|dev|canary) echo "UNKNOWN: $where: Google's 9 Nov 2026 posts name fixed Stable and Extended Stable builds only. Check the $ch channel build against Chrome Releases."; record 3; return;;
    *) echo "UNKNOWN: $where: channel \"$ch\" not recognised."; record 3; return;;
  esac
  if [ "$ch" = extended ] && { [ "$plat" = linux ] || [ "$plat" = android ]; }; then
    echo "UNKNOWN: $where: Google's Extended Stable post covers Windows and Mac only, not $plat."; record 3; return
  fi
  if [ -z "$ver" ] || ! valid_ver "$ver"; then
    echo "UNKNOWN: $where: version not recognised. Expect four numbers, for example 157.0.8103.50."; record 3; return
  fi
  fix_state "$ver"; st=$?
  if [ "$st" -eq 2 ]; then
    echo "UNKNOWN: $where: not a Chrome Stable or Extended Stable build number this pack can judge. Check it against Chrome Releases."; record 3; return
  fi
  if [ "$st" -eq 1 ]; then
    if [ "$ch" = extended ] && [ "${ver%%.*}" -lt 157 ]; then
      echo "VULNERABLE: $where: below the fixed Extended Stable build 156.0.8078.96. Update and relaunch Chrome."
    else
      echo "VULNERABLE: $where: below the fixed Stable build $(stable_fix "$plat"). Update and relaunch Chrome."
    fi
    record 1; return
  fi
  low=$(lowest_running "$(lc "$host")" chrome)
  if [ -n "$low" ] && { fix_state "$low"; [ $? -eq 1 ]; }; then
    echo "NEEDS RELAUNCH: $where: installed build has the fix, but a Chrome process is still running $low. Relaunch Chrome (close every window, or go to chrome://restart) so the fixed build loads."
    record 2; return
  fi
  if [ "$HAVE_RUN" -eq 1 ] && [ -n "$low" ]; then
    echo "PATCHED: $where: at or above the fixed build, and saved running processes are on a fixed build too."
  elif [ "$HAVE_RUN" -eq 1 ]; then
    echo "PATCHED: $where: at or above the fixed build. No saved running Chrome process for this host."
  else
    echo "PATCHED: $where: at or above the fixed build. Relaunch state not checked: confirm Chrome has been restarted."
  fi
  record 0
}

load_processes() {
  local f=$1 line h p v
  line=$(head -n1 "$f" | tr -d '\r'); line=${line#$'\xef\xbb\xbf'}
  [ "$(lc "$line")" = "host,process,version" ] || die "processes CSV header must be: host,process,version"
  while IFS= read -r line || [ -n "$line" ]; do
    line=${line%$'\r'}
    case $line in ''|\#*) continue;; esac
    IFS=, read -r h p v _ <<< "$line"
    h=$(lc "$(trim "${h:-}")"); p=$(norm_browser "${p:-}"); v=$(trim "${v:-}")
    valid_ver "$v" || continue
    RUNNING="$RUNNING$h|$p|$v"$'\n'
  done < <(tail -n +2 "$f")
}

check_inventory() {
  local f=$1 line h b c v rows=0
  line=$(head -n1 "$f" | tr -d '\r'); line=${line#$'\xef\xbb\xbf'}
  [ "$(lc "$line")" = "host,browser,channel,version" ] || die "CSV header must be: host,browser,channel,version"
  # count data rows first so an empty file prints nothing on stdout
  while IFS= read -r line || [ -n "$line" ]; do
    line=${line%$'\r'}; case $line in ''|\#*) continue;; esac; rows=$((rows+1))
  done < <(tail -n +2 "$f")
  [ "$rows" -gt 0 ] || die "CSV has no rows: $f"
  while IFS= read -r line || [ -n "$line" ]; do
    line=${line%$'\r'}
    case $line in ''|\#*) continue;; esac
    IFS=, read -r h b c v _ <<< "$line"
    h=$(trim "${h:-}"); b=$(norm_browser "${b:-}"); c=$(norm_channel "${c:-}"); v=$(trim "${v:-}")
    judge "${h:-host not given}" "$b" "$c" "$v" ""
  done < <(tail -n +2 "$f")
}

ver="" ch="" br="" plat="" run="" inv="" procs="" seen_ver=0 seen_run=0
while [ $# -gt 0 ]; do
  [ $# -ge 2 ] || usage
  case $1 in
    --version) ver=$2; seen_ver=1;;
    --channel) ch=$2;;
    --browser) br=$2;;
    --platform) plat=$(lc "$2");;
    --running) run=$2; seen_run=1;;
    --inventory) inv=$2;;
    --processes) procs=$2;;
    *) usage;;
  esac
  shift 2
done
if [ "$seen_ver" -eq 1 ] && [ -n "$inv" ]; then usage; fi
if [ "$seen_ver" -eq 0 ] && [ -z "$inv" ]; then usage; fi
if [ -n "$inv" ] && [ -n "$ch$br$plat$run" ]; then usage; fi
if [ "$seen_ver" -eq 1 ] && [ -n "$procs" ]; then usage; fi
case $plat in ''|windows|mac|linux|android) ;; *) die "--platform must be windows, mac, linux or android";; esac
HAVE_RUN=0
if [ "$seen_ver" -eq 1 ]; then
  ver=$(trim "$ver"); valid_ver "$ver" || die "--version must be four numbers, for example 157.0.8103.50"
  if [ "$seen_run" -eq 1 ]; then
    run=$(trim "$run"); valid_ver "$run" || die "--running must be four numbers, for example 157.0.8103.50"
    HAVE_RUN=1; RUNNING="input|$(norm_browser "${br:-chrome}")|$run"$'\n'
  fi
  judge input "$(norm_browser "${br:-chrome}")" "$(norm_channel "$ch")" "$ver" "$plat"
else
  if [ ! -f "$inv" ] || [ ! -r "$inv" ]; then die "cannot read: $inv"; fi
  if [ -n "$procs" ]; then
    if [ ! -f "$procs" ] || [ ! -r "$procs" ]; then die "cannot read: $procs"; fi
    HAVE_RUN=1; load_processes "$procs"
  fi
  check_inventory "$inv"
fi
case $worst in 3) v=VULNERABLE; rc=1;; 2) v='NEEDS RELAUNCH'; rc=2;; 1) v=UNKNOWN; rc=3;; *) v=PATCHED; rc=0;; esac
echo "RESULT: $v ($n checked: $c0 PATCHED, $c1 VULNERABLE, $c2 NEEDS RELAUNCH, $c3 UNKNOWN)"
exit "$rc"
