#!/bin/bash
# Inventory for CVE-2026-106378; optional deletion after typing "delete".
# Does not launch browsers. Removes whole .app bundles when applicable.
# Includes affected-range and unknown versions in the deletion prompt.
# Confirmation also stops processes running from the selected copies.
# Usage: bash find-affected-chrome.sh
set -u

fixed=155.0.8059.39
if [ "$#" -ne 0 ]; then
  printf 'This script takes no arguments; it scans only known browser caches.\n' >&2
  exit 2
fi

red= green= yellow= bold= reset=
if [ -t 1 ] && [ -z "${NO_COLOR+x}" ]; then
  red=$'\033[31m'
  green=$'\033[32m'
  yellow=$'\033[33m'
  bold=$'\033[1m'
  reset=$'\033[0m'
fi
targets=()
copies=()
versions=()
states=()

copy_name() {
  local name=${1##*/}
  case "$name" in
    headless_shell|chrome-headless-shell) printf 'Chrome Headless Shell' ;;
    *) printf '%s' "${name%.app}" ;;
  esac
}

cache_name() {
  case "$1" in
    */ms-playwright) printf 'Playwright' ;;
    */puppeteer) printf 'Puppeteer' ;;
    */selenium) printf 'Selenium' ;;
    */pyppeteer) printf 'Pyppeteer' ;;
  esac
}

# Match executable paths, including helpers inside a selected app bundle.
matching_pids() {
  local pid executable
  while read -r pid executable; do
    case "$executable" in
      "$1"|"$1"/*) printf '%s\n' "$pid" ;;
    esac
  done < <(/bin/ps -ww -U "$UID" -o pid=,comm=)
}

stop_copy() {
  local target=$1 pids pid current i
  for i in 0 1 2 3 4; do
    pids=$(matching_pids "$target")
    [ -n "$pids" ] || return 0
    if [ "$i" -eq 4 ]; then
      printf '  %sStill running; kept %s.%s\n' "$red" "$(copy_name "$target")" "$reset" >&2
      return 1
    fi
    if [ "$i" -eq 0 ] || [ "$i" -eq 3 ]; then
      if [ "$i" -eq 0 ]; then
        printf '  Stopping %s...\n' "$(copy_name "$target")"
      else
        printf '  Force-stopping %s...\n' "$(copy_name "$target")"
      fi
      for pid in $pids; do
        # Check again immediately before signalling, in case a PID was reused.
        current=$(/bin/ps -ww -p "$pid" -o comm= 2>/dev/null) || continue
        case "$current" in
          "$target"|"$target"/*)
            if [ "$i" -eq 0 ]; then
              kill -TERM "$pid" 2>/dev/null || true
            else
              kill -KILL "$pid" 2>/dev/null || true
            fi
            ;;
        esac
      done
    fi
    /bin/sleep 1
  done
}

roots=()
for cache in \
  "$HOME/Library/Caches/ms-playwright" \
  "$HOME/.cache/puppeteer" \
  "$HOME/.cache/selenium" \
  "$HOME/Library/Application Support/pyppeteer"; do
  [ -d "$cache" ] && [ ! -L "$cache" ] && roots+=("$cache")
done
if [ "${#roots[@]}" -eq 0 ]; then
  printf 'No supported browser cache directories found.\n'
  exit 0
fi

while IFS= read -r -d '' binary; do
  # Standard Chrome/Chromium bundles, including Puppeteer/Playwright copies.
  plist="${binary%/*}/../Info.plist"
  version=$(/usr/libexec/PlistBuddy -c 'Print :CFBundleShortVersionString' "$plist" 2>/dev/null) || version=
  status=UNKNOWN
  if [[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
    if /usr/bin/awk -v v="$version" -v f="$fixed" 'BEGIN {
      split(v, a, "."); split(f, b, ".");
      for (i = 1; i <= 4; i++) {
        if (a[i]+0 < b[i]+0) exit 0;
        if (a[i]+0 > b[i]+0) exit 1;
      }
      exit 1;
    }'; then
      status=AFFECTED-RANGE
    else
      status=AT-OR-ABOVE-FIX
    fi
  fi
  case "$binary" in
    *.app/Contents/MacOS/*) target=${binary%/Contents/MacOS/*} ;;
    *) target=$binary ;;
  esac
  duplicate=false
  for existing in "${copies[@]}"; do
    [ "$existing" = "$target" ] && duplicate=true
  done
  [ "$duplicate" = true ] && continue
  copies+=("$target")
  versions+=("$version")
  states+=("$status")
  if [ "$status" != AT-OR-ABOVE-FIX ]; then
    targets+=("$target")
  fi
done < <(/usr/bin/find "${roots[@]}" -type f \( \
  -name 'Google Chrome' -o -name 'Google Chrome for Testing' \
  -o -name 'Google Chrome Canary' -o -name 'Google Chrome Beta' \
  -o -name 'Google Chrome Dev' -o -name Chromium -o -name chrome \
  -o -name headless_shell -o -name chrome-headless-shell \
  \) ! -name '*Helper*' -perm +111 -print0 2>/dev/null)

printf '\n%sVulnerable Chrome cleanup%s\n' "$bold" "$reset"
if [ "${#copies[@]}" -eq 0 ]; then
  printf 'No cached Chrome copies found.\n'
  exit 0
fi
printf '%s to delete, %s to keep\n' "${#targets[@]}" "$((${#copies[@]} - ${#targets[@]}))"
for cache in "${roots[@]}"; do
  heading=false
  for ((index = 0; index < ${#copies[@]}; index++)); do
    target=${copies[index]}
    case "$target" in "$cache"/*) ;; *) continue ;; esac
    if [ "$heading" = false ]; then
      printf '\n%s%s%s\n' "$bold" "$(cache_name "$cache")" "$reset"
      printf '~/%s\n' "${cache#"$HOME/"}"
      heading=true
    fi
    printf '\n  %s\n' "$(copy_name "$target")"
    case "${states[index]}" in
      AFFECTED-RANGE) printf '  %sAffected%s · %s · Delete\n' "$red" "$reset" "${versions[index]}" ;;
      UNKNOWN) printf '  %sVersion unknown%s · Delete\n' "$yellow" "$reset" ;;
      *) printf '  %sAt or above fix%s · %s · Keep\n' "$green" "$reset" "${versions[index]}" ;;
    esac
    printf '  %s\n' "${target#"$cache/"}"
  done
done

if [ "${#targets[@]}" -gt 0 ]; then
  printf '\n%sDelete %s cached browser copies?%s\n' "$bold" "${#targets[@]}" "$reset"
  printf 'This stops their running processes and permanently deletes the copies.\n'
  printf 'Reinstall browsers before running automation.\n\n'
  printf 'Type delete to confirm, or press Return to cancel: '
  answer=
  IFS= read -r answer || answer=
  printf '\n'
  if [ "$answer" = delete ]; then
    if ! /bin/ps -ww -U "$UID" -o pid=,comm= >/dev/null 2>&1; then
      printf 'Cannot inspect running processes; nothing stopped or deleted.\n' >&2
      exit 1
    fi
    deleted=0
    for target in "${targets[@]}"; do
      stop_copy "$target" || continue
      if /bin/rm -rf -- "$target"; then
        printf '  %sDeleted%s %s\n' "$green" "$reset" "$(copy_name "$target")"
        deleted=$((deleted + 1))
      else
        printf '  %sCould not delete%s %s\n' "$red" "$reset" "$target" >&2
      fi
    done
    printf '\nDeleted %s of %s copies.\n' "$deleted" "${#targets[@]}"
  else
    printf 'Cancelled. No changes made.\n'
  fi
else
  printf '\nNo copies need deletion.\n'
fi
