#!/bin/sh
# Truvo Brotli self-check for CVE-2026-111497 (Node2Shell / HTTP2Shell / Broccoli2Shell)
# https://truvocyber.com/tools/brotli-vulnerability-checker
#
# Read-only. It runs version commands that are already installed on this
# machine, compares them with the fixed releases, and prints the result.
# It sends nothing anywhere, writes no files and needs no root.
#
# It checks only what is on PATH for the user running it. It cannot see
# inside containers, virtualenvs it is not run from, or self-contained .NET
# and bun --compile apps, which carry their own copy and must be rebuilt.
#
# Fixed versions last checked 2026-10-27. Sources:
#   Node.js  https://nodejs.org/en/blog/vulnerability/october-2026-security-releases
#   .NET     https://github.com/dotnet/announcements/issues/494
#   Bun      https://bun.com/blog/bun-v1.4.4
#   Python   https://github.com/advisories/GHSA-9v8h-qqgm-4m35
#   Envoy    https://github.com/envoyproxy/envoy/security/advisories/GHSA-3wvj-j6qm-62mr
#   Brotli   https://github.com/google/brotli/releases
#
# Usage:  sh brotli-check.sh        Exit code 1 if anything is affected.

AFFECTED=0

# ver_lt A B: true when version A is lower than B (numeric, dot separated).
ver_lt() {
  awk -v a="$1" -v b="$2" 'BEGIN {
    n = split(a, x, "."); m = split(b, y, "."); k = (n > m) ? n : m
    for (i = 1; i <= k; i++) { xi = x[i] + 0; yi = y[i] + 0
      if (xi < yi) exit 0; if (xi > yi) exit 1 }
    exit 1 }'
}

clean() { printf '%s' "$1" | sed -E 's/^[^0-9]*//; s/[^0-9.].*$//'; }

say() { printf '%-12s %-24s %s\n' "$1" "$2" "$3"; }

report() { # name version fixed-or-empty note
  name=$1; ver=$2; fixed=$3; note=$4
  if [ -z "$fixed" ]; then say "[NO FIX]" "$name $ver" "$note"; AFFECTED=1
  elif ver_lt "$ver" "$fixed"; then say "[AFFECTED]" "$name $ver" "upgrade to $fixed or later. $note"; AFFECTED=1
  else say "[OK]" "$name $ver" "at or after $fixed. $note"; fi
}

missing() { say "[--]" "$1" "not found on PATH"; }

echo "Truvo Brotli self-check, CVE-2026-111497"
echo "Host: $(hostname 2>/dev/null)   User: $(id -un 2>/dev/null)   $(date -u +%Y-%m-%dT%H:%M:%SZ)"
echo

# Node.js: fixed 26.12.1, 24.22.1, 22.23.4. Other lines get no fix.
if command -v node >/dev/null 2>&1; then
  v=$(clean "$(node -v 2>/dev/null)"); major=${v%%.*}
  case $major in
    26) report "Node.js" "$v" 26.12.1 "Rebuild container images too." ;;
    24) report "Node.js" "$v" 24.22.1 "Rebuild container images too." ;;
    22) report "Node.js" "$v" 22.23.4 "Rebuild container images too." ;;
    *) if [ "$major" -gt 26 ] 2>/dev/null; then report "Node.js" "$v" 26.12.1 ""
       else report "Node.js" "$v" "" "line $major gets no fix. Move to 22, 24 or 26."; fi ;;
  esac
else missing "Node.js"; fi

# Bun: fixed 1.4.4. Rebuild bun build --compile executables.
if command -v bun >/dev/null 2>&1; then
  report "Bun" "$(clean "$(bun -v 2>/dev/null)")" 1.4.4 "Rebuild any bun --compile executables."
else missing "Bun"; fi

# .NET runtimes: fixed 10.0.14, 9.0.22, 8.0.33. Framework is not affected.
if command -v dotnet >/dev/null 2>&1; then
  found=0
  for v in $(dotnet --list-runtimes 2>/dev/null | awk '/Microsoft.NETCore.App/ {print $2}'); do
    found=1; major=${v%%.*}; base=$(clean "$v")
    case $major in
      10) report ".NET runtime" "$v" 10.0.14 "" ;;
      9) report ".NET runtime" "$v" 9.0.22 "" ;;
      8) report ".NET runtime" "$v" 8.0.33 "" ;;
      11) case $v in *preview*|*rc.1.*|*rc.2.*|*rc.1|*rc.2) say "[AFFECTED]" ".NET runtime $v" "update to 11.0.0-rc.3 or later."; AFFECTED=1 ;;
          *) say "[OK]" ".NET runtime $v" "at or after 11.0.0-rc.3." ;; esac ;;
      *) if [ "$major" -lt 8 ] 2>/dev/null; then report ".NET runtime" "$base" "" "end of life, no fix."; fi ;;
    esac
  done
  [ $found = 0 ] && say "[--]" ".NET runtime" "dotnet found but no Microsoft.NETCore.App runtime listed"
  echo "             Self-contained and NativeAOT apps carry their own copy: rebuild them with a patched SDK."
else missing ".NET"; fi

# PowerShell 7: no confirmed fixed build yet.
if command -v pwsh >/dev/null 2>&1; then
  report "PowerShell" "$(clean "$(pwsh -v 2>/dev/null)")" "" "7.4 to 7.6 are named as affected and no fix is confirmed yet. Watch the PowerShell releases page."
else missing "PowerShell 7"; fi

# Envoy: fixed 1.40.0, 1.39.3, 1.38.6, 1.37.8, 1.36.12. Only if the Brotli decompressor is configured.
if command -v envoy >/dev/null 2>&1; then
  v=$(envoy --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1)
  case $v in
    1.40.*) report "Envoy" "$v" 1.40.0 "" ;;
    1.39.*) report "Envoy" "$v" 1.39.3 "" ;;
    1.38.*) report "Envoy" "$v" 1.38.6 "" ;;
    1.37.*) report "Envoy" "$v" 1.37.8 "" ;;
    1.36.*) report "Envoy" "$v" 1.36.12 "" ;;
    *) if ver_lt "$v" 1.36.0; then report "Envoy" "$v" "" "line gets no fix."; else report "Envoy" "$v" 1.40.0 ""; fi ;;
  esac
  echo "             Envoy is affected only when the Brotli decompressor filter is configured."
else missing "Envoy"; fi

# Python packages: Brotli fixed 1.2.1, brotlicffi fixed 1.2.1.0 (affected from 1.0.3).
for py in python3 python; do
  command -v "$py" >/dev/null 2>&1 || continue
  for pkg in brotli brotlicffi; do
    v=$("$py" -m pip show "$pkg" 2>/dev/null | awk '/^Version:/ {print $2}')
    if [ -z "$v" ]; then say "[--]" "$py $pkg" "not installed for $py"; continue; fi
    fixed=1.2.1; [ "$pkg" = brotlicffi ] && fixed=1.2.1.0
    if ver_lt "$v" 1.0.3; then say "[OK]" "$py $pkg $v" "older than the affected range, but very old. Upgrade."
    else report "$py $pkg" "$v" "$fixed" "Check each virtualenv too."; fi
  done
  break
done

# System Brotli library. Distros patch without changing the version, so look
# for the CVE in the local package changelog.
if command -v dpkg-query >/dev/null 2>&1 && dpkg-query -W libbrotli1 >/dev/null 2>&1; then
  v=$(dpkg-query -W -f='${Version}' libbrotli1)
  if zcat /usr/share/doc/libbrotli1/changelog.Debian.gz 2>/dev/null | grep -q CVE-2026-111497; then
    say "[OK]" "libbrotli1 $v" "package changelog names CVE-2026-111497."
  else say "[CHECK]" "libbrotli1 $v" "changelog does not name CVE-2026-111497. Run your package updates."; AFFECTED=1; fi
elif command -v rpm >/dev/null 2>&1 && rpm -q brotli >/dev/null 2>&1; then
  v=$(rpm -q --qf '%{VERSION}-%{RELEASE}' brotli)
  if rpm -q --changelog brotli 2>/dev/null | grep -q CVE-2026-111497; then
    say "[OK]" "brotli $v" "package changelog names CVE-2026-111497."
  else say "[CHECK]" "brotli $v" "changelog does not name CVE-2026-111497. Run your package updates."; AFFECTED=1; fi
elif command -v brew >/dev/null 2>&1 && brew list --versions brotli >/dev/null 2>&1; then
  v=$(brew list --versions brotli | awk '{print $2}')
  case $v in 1.2.*) report "brotli (brew)" "$v" 1.2.1 "" ;; 1.1.*) report "brotli (brew)" "$v" 1.1.1 "" ;; *) report "brotli (brew)" "$v" 1.0.10 "" ;; esac
else missing "system libbrotli"; fi

echo
if [ $AFFECTED = 1 ]; then
  echo "Result: something needs attention. See https://truvocyber.com/incidents/brotli-cve-2026-111497"
  exit 1
fi
echo "Result: nothing affected found on this machine's PATH. Check containers and other hosts too."
exit 0
