#!/bin/sh
# litellm-check.sh — Zero Hunt Research, free self-check (MIT licence)
# https://zerohunt.ai/self-check/
#
# Looks for traces of the compromised LiteLLM 1.107.0 (PyPI + Docker, 21 Oct
# 2026) WITHOUT starting Python: on an infected host every python or pip
# command runs the credential stealer again (cook.pth executes at start-up).
#
# Read-only and offline: find, grep, ls, sha256sum and, if present, docker and
# kubectl in read mode. It changes nothing and sends nothing anywhere.
#
#   sh litellm-check.sh              # this host (run as root to search all of /)
#   sh litellm-check.sh /path/repos  # also scan lock and requirements files there
#
# Exit code: 0 nothing found, 1 evidence found.
# Finding nothing does not prove you were not exposed: containers and /tmp
# clean-up erase the markers. Check your lock files, CI logs and egress logs.

VERSION=0.1.0
[ "$1" = "--version" ] && { echo "litellm-check $VERSION"; exit 0; }
REPOS="$1"
HIT=0
say() { printf '%s\n' "$*"; }

say "litellm-check $VERSION — LiteLLM 1.107.0 compromise self-check (no Python started)"
say ""
say "== Payload files and 1.107.0 metadata in any site-packages"
F=$(find / \( -path /proc -o -path /sys -o -path /dev -o -path /run \) -prune -o \( -name cook.pth -o -name cook.py -o -name 'litellm-1.107.0.dist-info' \) -path '*-packages*' -print 2>/dev/null)
if [ -n "$F" ]; then say "$F" | sed 's/^/  FOUND /'; HIT=1; else say "  none"; fi

say ""
say "== sitecustomize.py matching the published payload hash"
S=$(find / \( -path /proc -o -path /sys -o -path /dev -o -path /run \) -prune -o -name sitecustomize.py -path '*-packages*' -exec sha256sum {} + 2>/dev/null | grep 34bef04fe88ac9d0)
if [ -n "$S" ]; then say "$S" | sed 's/^/  FOUND /'; HIT=1; else say "  none (a legitimate sitecustomize.py with another hash is normal)"; fi

say ""
say "== Markers left by the stealer and the hourly loader"
MK=0
for m in /tmp/.pc_once_72129118c59f5636c0bb /tmp/remote_code_loader.lock; do
  if [ -e "$m" ]; then say "  FOUND $m"; HIT=1; MK=1; fi
done
if command -v lsof >/dev/null 2>&1 && [ -e /tmp/remote_code_loader.lock ]; then
  lsof /tmp/remote_code_loader.lock 2>/dev/null | sed 's/^/  LOADER RUNNING: /'
fi
[ "$MK" = 0 ] && say "  none"

say ""
say "== Cached 1.107.0 wheels (pip, uv)"
C=$(find / \( -path /proc -o -path /sys -o -path /dev -o -path /run \) -prune -o -iname 'litellm-1.107.0*' \( -path '*/.cache/*' -o -path '*/pip/*' -o -path '*/uv/*' \) -print 2>/dev/null | head -20)
if [ -n "$C" ]; then say "$C" | sed 's/^/  FOUND /'; HIT=1; else say "  none"; fi

if command -v docker >/dev/null 2>&1; then
  say ""
  say "== Docker images (compromised index sha256:1bbcb1443bc962e45f...)"
  D=$(docker images --digests 2>/dev/null | grep -i litellm)
  if [ -n "$D" ]; then
    say "$D" | sed 's/^/  /'
    printf '%s' "$D" | grep -q 1bbcb1443bc962e45f && { say "  FOUND the compromised image digest"; HIT=1; }
    printf '%s' "$D" | grep -qE 'v1\.107\.0' && { say "  FOUND a v1.107.0 tag"; HIT=1; }
  else
    say "  no LiteLLM images"
  fi
fi

if command -v kubectl >/dev/null 2>&1; then
  say ""
  say "== Kubernetes pods running the compromised image"
  K=$(kubectl get pods -A -o jsonpath='{range .items[*]}{.metadata.namespace}/{.metadata.name} {range .status.containerStatuses[*]}{.imageID} {end}{"\n"}{end}' 2>/dev/null | grep -i 1bbcb1443bc962e45f)
  if [ -n "$K" ]; then say "$K" | sed 's/^/  FOUND /'; HIT=1; else say "  none (or no cluster access)"; fi
fi

if [ -n "$REPOS" ] && [ -d "$REPOS" ]; then
  say ""
  say "== Lock and requirements files under $REPOS"
  R=$(grep -rnE 'litellm(\[[^]]*\])?==1\.107\.0' --include='requirements*.txt' "$REPOS" 2>/dev/null)
  L=$(grep -rn -A3 --include='uv.lock' --include='poetry.lock' --include='Pipfile.lock' -E '"?litellm"?' "$REPOS" 2>/dev/null | grep '1\.107\.0')
  if [ -n "$R$L" ]; then say "$R"; say "$L"; HIT=1; else say "  no pin or lock entry for 1.107.0"; fi
fi

say ""
if [ "$HIT" = 1 ]; then
  say "Result: EVIDENCE FOUND. Do not run python or pip in that environment."
  say "Clean up in the vendor's order and rotate every reachable secret from a clean machine:"
else
  say "Result: no trace found on this host. Not finding traces proves nothing about the window; check lock files, CI logs and egress (litellm.dev, /codepy)."
fi
say "https://zerohunt.ai/blog/litellm-1-107-0-pypi-supply-chain-compromise-remediation/#remediation"
say "Zero Hunt tests what an attacker could reach with the stolen secrets, on-premise: https://zerohunt.ai/on-premise-ai-red-team/"
exit "$HIT"
