#!/usr/bin/env bash
# check-litellm-1107.sh - READ-ONLY checker for the LiteLLM 1.107.0 PyPI/Docker supply-chain compromise (21 Oct 2026, no CVE)
# Sources: https://docs.litellm.ai/blog/security-incident-oct-21-2026 (vendor)
#          Independent analysis by security researchers
# Never runs any Python interpreter or pip found on the host (that would execute cook.pth / sitecustomize.py).
# Never writes, deletes, kills or changes anything.
#
# Usage: sudo bash check-litellm-1107.sh [PROJECT_PATH ...]     (lockfile scan paths; default: /home /root /srv /opt /app /workspace)
# Env:   PY_ROOTS="/"   roots searched for site-packages/dist-packages (default /)
#        CHECK_DOCKER=1 / CHECK_PROC=1
# Exit:  0 = CLEAN          nothing found
#        1 = COMPROMISED    malicious install/payload/runtime artefact found
#        2 = UNKNOWN        something needs manual review (e.g. litellm image pulled 21 Oct on a floating tag)
#        3 = REFERENCED_ONLY 1.107.0 referenced in a lockfile/manifest/cache but not installed
set -u
export LC_ALL=C

# ---- IOCs (verbatim from security researcher analysis and LiteLLM) ----
BAD_FILE_SHA256="7f191632e7320705d154b85204f1ae546b543acbd7e8861229b76f5042b92bdb 84dc8c82c0a4a4a62552ef826b49fcc86d1a5c4a5d0f2c06665723f93d129d7d 9dd0a3926f96a37eb86cf1ee2310d9be93139cc4ebef3ef9e2efe9531390be25 03ac2a8b752605d1d7710979c17ff18b03dd978203ba23ba97fd8ce3d6f897ea 34bef04fe88ac9d0aae0475630b1738681d15b94f233ac8aa46eacf5968305fa b6122440d0d9cb1eb1ade9933f49214da8c31288de7710c076c2680dfa528465"
BAD_ARTIFACT_SHA256="7598727f209d717041db37f7dca271d3a7d328a279890e0e1d55375729aeb86d 0aaf7ba346d41ef2f40390436738b001180d5a1c03aa093f121078a9969d9a1a 668169d35b87bf51324080390b1b1c5ab12bd335cfe5f1a99e3689abd5e02ae6 462d0c9f67ca522d9e0e4acd245e90b36ce0d5353604a66bb7c43a1ec4a6fcf7 c2e32d0ce2548aa662c18bf5001911a79fe5b30cf8357132275add02fdcde054 1bbce8caa94f01c0aaa0b7ddae51553a8a32e1ce53d96d44fe583272aff7ad00 12d4ccc5c6f8d35cf4f2cf233b7f3e418e863e657d81ff5f5fc9e210005b9484 2befe2a79fb4fbbd9a543b5b40b530c8cffa8771bab22b8fb22b2e4ae3b1f819"
# full digests: GHCR/Docker Hub index, amd64 manifest, arm64 manifest; prefixes: litellm-database, litellm-non_root index (only truncated values published)
BAD_DIGESTS="1bbcb1443bc962e45f5aad53fe3d2c30600e0379715fb6c664cdd3ac126c339e bd1aeee419ea36da9aa2b657288da4101887be468f020ac63d22ba6ed84a3ea5 56b74573eed6e4aafe4b50603dbe7853e0345f188c8e44a09f1098caf1f4bcdf"
BAD_DIGEST_PREFIX="c00f9d92c427 7b00e91cd2fe"
MARKERS="/tmp/.pc_once_72129118c59f5636c0bb /tmp/remote_code_loader.lock"
NET_RE='litellm\.dev|letters9813|pythonclient-test|XWiA8gZNa7NcX2XRoAEb'

COMP=0; UNK=0; REF=0
say() { printf '%s\n' "$*"; }
hdr() { printf '\n==== %s ====\n' "$*"; }
comp() { COMP=$((COMP+1)); say "[COMPROMISED] $*"; }
unk()  { UNK=$((UNK+1));  say "[UNKNOWN] $*"; }
ref()  { REF=$((REF+1));  say "[REFERENCED_ONLY] $*"; }
inlist() { case " $2 " in *" $1 "*) return 0 ;; esac; return 1; }
sha() { sha256sum "$1" 2>/dev/null | cut -d' ' -f1; }

# ---------- 1. Python environments ----------
hdr "Python environments (site-packages / dist-packages)"
read -r -a PYR <<< "${PY_ROOTS:-/}"
PRUNE=( -path /proc -o -path /sys -o -path /dev -o -path /run -o -path /var/lib/docker -o -path /var/lib/containerd -o -path /snap )
NENV=0
while IFS= read -r -d '' sp; do
  NENV=$((NENV+1))
  for di in "$sp"/litellm-*.dist-info "$sp"/litellm-*.egg-info; do
    [ -d "$di" ] || continue
    v=$(grep -m1 -i '^Version:' "$di/METADATA" "$di/PKG-INFO" 2>/dev/null | head -n1 | sed 's/.*Version:[[:space:]]*//I' | tr -d '\r')
    if [ "$v" = "1.107.0" ]; then comp "$di : litellm 1.107.0 installed"
    else say "  ok: $di (litellm ${v:-?})"; fi
    grep -qE '(^|/)(cook\.pth|cook\.py|sitecustomize\.py),' "$di/RECORD" 2>/dev/null && comp "$di/RECORD lists cook.pth/cook.py/sitecustomize.py"
  done
  for f in "$sp/cook.pth" "$sp/cook.py"; do
    [ -e "$f" ] || continue
    h=$(sha "$f")
    if inlist "$h" "$BAD_FILE_SHA256"; then comp "$f (sha256 $h matches published IOC)"
    elif [ "${f##*/}" = cook.pth ] && grep -q '^import cook' "$f" 2>/dev/null; then comp "$f contains 'import cook' (sha256 $h)"
    else unk "$f present, hash not in IOC list (sha256 $h) - review"; fi
  done
  if [ -e "$sp/sitecustomize.py" ]; then
    h=$(sha "$sp/sitecustomize.py")
    if inlist "$h" "$BAD_FILE_SHA256"; then comp "$sp/sitecustomize.py (sha256 $h matches published IOC)"
    elif grep -qE 'remote_code_loader|letters9813|pythonclient-test|PRIVCOLLECT' "$sp/sitecustomize.py" 2>/dev/null; then comp "$sp/sitecustomize.py contains loader strings"
    else say "  note: $sp/sitecustomize.py exists (not an IOC match; distros ship their own)"; fi
  fi
  for c in "$sp"/__pycache__/cook.*.pyc; do [ -e "$c" ] && comp "$c (compiled payload left behind)"; done
  for p in "$sp"/*.pth; do
    [ -e "$p" ] || continue; [ "${p##*/}" = cook.pth ] && continue
    grep -qE '^import[[:space:]]+cook\b' "$p" 2>/dev/null && comp "$p imports cook"
  done
done < <(find "${PYR[@]}" \( "${PRUNE[@]}" \) -prune -o -type d \( -name site-packages -o -name dist-packages \) -print0 2>/dev/null)
say "Environments scanned: $NENV (system, venv, conda, pipx, ~/.local, /app/.venv ...)"

# ---------- 2. Host artefacts ----------
hdr "Host artefacts"
for m in $MARKERS; do [ -e "$m" ] && comp "$m exists (stealer marker / loader lock)"; done
for d in /tmp/.pc_sq_* /tmp/.pc_e_* /var/tmp/.pc_sq_* /var/tmp/.pc_e_*; do [ -e "$d" ] && comp "$d (privcollect temp dir)"; done
CROOTS=(); for d in /root /home; do [ -d "$d" ] && CROOTS+=("$d"); done; [ "${PY_ROOTS:-/}" != / ] && CROOTS+=("${PYR[@]}")
CACHE=""; [ ${#CROOTS[@]} -gt 0 ] && CACHE=$(find "${CROOTS[@]}" -maxdepth 8 \( "${PRUNE[@]}" \) -prune -o \( -path '*/.cache/*' -o -path '*/pip/*' -o -path '*/uv/*' \) -iname 'litellm-1.107.0*' -print 2>/dev/null | sort -u | head -n 20)
[ -n "$CACHE" ] && ref "package cache holds 1.107.0 artefacts:" && printf '%s\n' "$CACHE" | sed 's/^/      /'

# ---------- 3. Lockfiles and manifests ----------
hdr "Lockfiles and manifests"
if [ $# -gt 0 ]; then SCAN=("$@"); else SCAN=(); for d in /home /root /srv /opt /app /workspace; do [ -d "$d" ] && SCAN+=("$d"); done; fi
say "Scanning: ${SCAN[*]:-<none>}"
VERRE='litellm(\[[^]]*\])?[[:space:]]*(===?|~=|@)?[[:space:]]*["'\'']?v?1\.107\.0([^0-9.a-z]|$)'
HASHRE=$(printf '%s %s %s' "$BAD_ARTIFACT_SHA256" "$BAD_DIGESTS" "$BAD_DIGEST_PREFIX" | tr ' ' '|')
NLOCK=0
if [ ${#SCAN[@]} -gt 0 ]; then
while IFS= read -r -d '' f; do
  NLOCK=$((NLOCK+1)); hit=""
  case "${f##*/}" in
    poetry.lock|uv.lock|pdm.lock)
      awk 'BEGIN{n=0} /^\[\[package\]\]/{n=0} /^name = "litellm"$/{n=1} n&&/^version = "1\.107\.0"$/{found=1} END{exit !found}' "$f" && hit="litellm 1.107.0 locked" ;;
    Pipfile.lock)
      tr -d ' \n' < "$f" | grep -q '"litellm":{[^}]*"version":"==1\.107\.0"' && hit="litellm ==1.107.0 locked" ;;
    *) grep -Eqi "$VERRE" "$f" && hit="litellm 1.107.0 referenced" ;;
  esac
  grep -Eqi 'litellm(-database|-non_root)?:v?1\.107\.0' "$f" && hit="${hit:+$hit; }image tag v1.107.0"
  grep -Eqi "$HASHRE" "$f" && hit="${hit:+$hit; }malicious artefact hash/digest"
  [ -n "$hit" ] && ref "$f : $hit"
done < <(find "${SCAN[@]}" \( -name node_modules -o -name .git -o -name site-packages -o -name dist-packages \) -prune -o -type f \( -name 'requirements*.txt' -o -name 'requirements*.in' -o -name 'constraints*.txt' -o -name poetry.lock -o -name uv.lock -o -name pdm.lock -o -name Pipfile.lock -o -name Pipfile -o -name pyproject.toml -o -name setup.cfg -o -name 'Dockerfile*' -o -name '*compose*.y*ml' -o -name 'values*.y*ml' \) -print0 2>/dev/null)
fi
say "Manifests scanned: $NLOCK"

# ---------- 4. Docker ----------
if [ "${CHECK_DOCKER:-1}" = 1 ] && command -v docker >/dev/null 2>&1 && docker info >/dev/null 2>&1; then
  hdr "Docker images and containers"
  digre=$(printf '%s %s' "$BAD_DIGESTS" "$BAD_DIGEST_PREFIX" | tr ' ' '|')
  while read -r repo tag dig id; do
    case "$repo" in *berriai/litellm*|*litellm/litellm*) ;; *) continue ;; esac
    rd=$(docker image inspect -f '{{join .RepoDigests " "}}' "$id" 2>/dev/null)
    created=$(docker image inspect -f '{{.Created}}' "$id" 2>/dev/null)
    if printf '%s %s' "$dig" "$rd" | grep -Eq "$digre"; then comp "image $repo:$tag ($id) digest matches published malicious digest"
    elif [ "$tag" = v1.107.0 ] || [ "$tag" = 1.107.0 ]; then comp "image $repo:$tag ($id)"
    elif case "$tag" in latest|main-latest|main-stable|'<none>') true ;; *) false ;; esac && case "$created" in 2026-10-21*) true ;; *) false ;; esac; then
      unk "image $repo:$tag ($id) is a floating tag built $created - verify digest against the IOC list"
    else say "  ok: $repo:$tag ($id) created $created"; fi
  done < <(docker images --digests --format '{{.Repository}} {{.Tag}} {{.Digest}} {{.ID}}' 2>/dev/null)
  while read -r cid img; do
    [ -n "$cid" ] || continue
    out=$(docker exec "$cid" sh -c 'for d in /app/.venv/lib/python3*/site-packages /usr/local/lib/python3*/site-packages /usr/lib/python3*/site-packages; do ls "$d"/cook.pth "$d"/cook.py "$d"/litellm-1.107.0.dist-info 2>/dev/null; done; ls /tmp/.pc_once_72129118c59f5636c0bb /tmp/remote_code_loader.lock 2>/dev/null' 2>/dev/null)
    if [ -n "$out" ]; then comp "container $cid ($img):"; printf '%s\n' "$out" | sed 's/^/      /'
    else say "  ok: container $cid ($img) - no payload files in standard site-packages"; fi
  done < <(docker ps --format '{{.ID}} {{.Image}}' 2>/dev/null)
fi

# ---------- 5. Processes ----------
if [ "${CHECK_PROC:-1}" = 1 ] && [ -d /proc/1 ]; then
  hdr "Running processes"
  for p in /proc/[0-9]*; do
    pid=${p#/proc/}; [ "$pid" = "$$" ] && continue
    cl=$(tr '\0' ' ' < "$p/cmdline" 2>/dev/null) || continue
    [ -n "$cl" ] || continue
    if tr '\0' '\n' < "$p/environ" 2>/dev/null | grep -qx 'PRIVCOLLECT_ACTIVE=1'; then comp "pid $pid has PRIVCOLLECT_ACTIVE=1 in its environment: $cl"; fi
    for fd in "$p"/fd/*; do
      if [ "$(readlink "$fd" 2>/dev/null)" = /tmp/remote_code_loader.lock ]; then comp "pid $pid holds /tmp/remote_code_loader.lock (remote code loader daemon): $cl"; break; fi
    done
    case "$cl" in *python*)
      printf '%s' "$cl" | grep -Eq 'cook|sitecustomize|remote_code_loader|privcollect|codepy|letters9813' && unk "python pid $pid suspicious cmdline: $cl" ;;
    esac
  done
  [ "$(id -u)" != 0 ] && say "(not root: other users' process environments and fds were not readable)"
fi

# ---------- 6. DNS / hosts / logs (cheap) ----------
hdr "DNS, hosts and log traces"
grep -Eiq "$NET_RE" /etc/hosts 2>/dev/null && unk "/etc/hosts references litellm.dev (sinkhole or attacker entry?): $(grep -Ei "$NET_RE" /etc/hosts | tr '\n' ' ')"
for f in /var/log/syslog /var/log/messages /var/log/dnsmasq.log /var/log/named/*.log /var/log/squid/access.log /var/log/nginx/access.log; do
  [ -r "$f" ] || continue
  n=$(tail -n 500000 "$f" 2>/dev/null | grep -Eci "$NET_RE")
  [ "${n:-0}" -gt 0 ] && comp "$f has $n lines referencing litellm.dev / letters9813: $(tail -n 500000 "$f" | grep -Ei "$NET_RE" | tail -n 2 | tr '\n' ' ')"
done
if command -v journalctl >/dev/null 2>&1; then
  j=$(journalctl --no-pager -q --since 2026-10-21 2>/dev/null | grep -Ei "$NET_RE" | tail -n 3)
  [ -n "$j" ] && comp "journald references litellm.dev / letters9813: $j"
fi
say "(DNS resolver caches are not inspected on Linux; query your DNS/proxy logs with the KQL in this pack.)"

# ---------- summary ----------
hdr "Summary"
say "COMPROMISED findings: $COMP | UNKNOWN: $UNK | REFERENCED_ONLY: $REF"
if [ "$COMP" -gt 0 ]; then say "OVERALL: COMPROMISED - treat the host as compromised: isolate, rebuild, rotate every reachable credential FROM A CLEAN MACHINE. Do not run pip/python here first."; exit 1
elif [ "$UNK" -gt 0 ]; then say "OVERALL: UNKNOWN - review the items above"; exit 2
elif [ "$REF" -gt 0 ]; then say "OVERALL: REFERENCED_ONLY - 1.107.0 is referenced but not installed here; pin litellm==1.106.2 by hash and check every machine/CI job that used these files"; exit 3
else say "OVERALL: CLEAN - no indicators found (absence of artefacts is not proof; containers and /tmp cleanup remove markers)"; exit 0; fi
