Last active
March 27, 2026 16:54
-
-
Save sreejithbnaick/d548b10300fe73439c482b6a9067ea61 to your computer and use it in GitHub Desktop.
Shell script to check litellm 1.82.7/1.82.8 malware compromise on your machine
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env bash | |
| # ============================================================================= | |
| # check_litellm_compromise.sh | |
| # Detects compromised litellm versions (1.82.7 & 1.82.8) across: | |
| # - System Python / pip / pyenv / conda / pipx / uv | |
| # - Malware persistence artifacts (sysmon backdoor, litellm_init.pth) | |
| # - [Optional] All venvs and PyPI modules under a given workspace directory | |
| # | |
| # Incident: TeamPCP supply-chain attack — March 24 2026 | |
| # | |
| # USAGE: | |
| # ./check_litellm_compromise.sh # system check only | |
| # ./check_litellm_compromise.sh /path/to/workspace # system + workspace scan | |
| # ./check_litellm_compromise.sh --help | |
| # ============================================================================= | |
| set -euo pipefail | |
| # ── Usage / help ────────────────────────────────────────────────────────────── | |
| usage() { | |
| echo "" | |
| echo " Usage: $(basename "$0") [WORKSPACE_DIR]" | |
| echo "" | |
| echo " Arguments:" | |
| echo " WORKSPACE_DIR (optional) Root folder to recursively scan for" | |
| echo " Python venvs, pip packages, and dependency files." | |
| echo " If omitted, only the system-level check is performed." | |
| echo "" | |
| echo " Examples:" | |
| echo " $(basename "$0") # system check only" | |
| echo " $(basename "$0") /Users/sreejith/workspace" | |
| echo " $(basename "$0") ~/projects" | |
| echo "" | |
| exit 0 | |
| } | |
| [[ "${1:-}" == "--help" || "${1:-}" == "-h" ]] && usage | |
| # ── Arguments ───────────────────────────────────────────────────────────────── | |
| WORKSPACE_DIR="${1:-}" # empty → system-only mode | |
| SCAN_WORKSPACE=false | |
| if [[ -n "$WORKSPACE_DIR" ]]; then | |
| WORKSPACE_DIR="${WORKSPACE_DIR%/}" # strip trailing slash | |
| SCAN_WORKSPACE=true | |
| fi | |
| # ── Colours ────────────────────────────────────────────────────────────────── | |
| RED='\033[0;31m' | |
| YLW='\033[1;33m' | |
| GRN='\033[0;32m' | |
| CYN='\033[0;36m' | |
| BLD='\033[1m' | |
| DIM='\033[2m' | |
| RST='\033[0m' | |
| COMPROMISED_VERSIONS=("1.82.7" "1.82.8") | |
| # ── Tracking ───────────────────────────────────────────────────────────────── | |
| FOUND_COMPROMISED=0 | |
| FOUND_LITELLM_LOCATIONS=() | |
| COMPROMISED_LOCATIONS=() | |
| # ── Helpers ────────────────────────────────────────────────────────────────── | |
| banner() { | |
| echo "" | |
| echo -e "${CYN}${BLD}══════════════════════════════════════════════════════${RST}" | |
| echo -e "${CYN}${BLD} $1${RST}" | |
| echo -e "${CYN}${BLD}══════════════════════════════════════════════════════${RST}" | |
| } | |
| section_note() { | |
| echo -e "${DIM} $1${RST}" | |
| } | |
| is_compromised() { | |
| local ver="$1" | |
| for cv in "${COMPROMISED_VERSIONS[@]}"; do | |
| [[ "$ver" == "$cv" ]] && return 0 | |
| done | |
| return 1 | |
| } | |
| # check_litellm_in_pip LABEL PIP_BINARY | |
| # Queries pip for litellm, prints result, updates global tracking arrays. | |
| check_litellm_in_pip() { | |
| local label="$1" | |
| local pip_cmd="$2" | |
| # Accept both commands on PATH and absolute paths | |
| if ! command -v "$pip_cmd" &>/dev/null 2>&1 && [[ ! -x "$pip_cmd" ]]; then | |
| return | |
| fi | |
| local raw | |
| raw=$("$pip_cmd" show litellm 2>/dev/null || true) | |
| if [[ -z "$raw" ]]; then | |
| echo -e " ${GRN}✔ litellm not installed${RST} ${DIM}[${label}]${RST}" | |
| return | |
| fi | |
| local ver location | |
| ver=$(echo "$raw" | awk '/^Version:/{print $2}') | |
| location=$(echo "$raw" | awk '/^Location:/{print $2}') | |
| FOUND_LITELLM_LOCATIONS+=("${label} → v${ver} @ ${location}") | |
| if is_compromised "$ver"; then | |
| FOUND_COMPROMISED=1 | |
| COMPROMISED_LOCATIONS+=("${label} → v${ver} @ ${location}") | |
| echo -e " ${RED}${BLD}✘ COMPROMISED litellm==${ver}${RST}" | |
| echo -e " ${RED}Env : ${label}${RST}" | |
| echo -e " ${RED}Path : ${location}${RST}" | |
| else | |
| echo -e " ${YLW}⚠ litellm==${ver} installed (not a known-bad version)${RST}" | |
| echo -e " ${DIM}Env : ${label}${RST}" | |
| echo -e " ${DIM}Path : ${location}${RST}" | |
| fi | |
| } | |
| # check_pth_artifact SITE_PACKAGES_DIR | |
| # Looks for litellm_init.pth — the auto-exec payload dropped by v1.82.8. | |
| check_pth_artifact() { | |
| local site_pkgs="$1" | |
| local pth_file="${site_pkgs}/litellm_init.pth" | |
| if [[ -f "$pth_file" ]]; then | |
| FOUND_COMPROMISED=1 | |
| COMPROMISED_LOCATIONS+=("Malware .pth artifact: ${pth_file}") | |
| echo -e " ${RED}${BLD}✘ MALWARE ARTIFACT: ${pth_file}${RST}" | |
| echo -e " ${RED} Executes on EVERY Python startup — remove immediately!${RST}" | |
| fi | |
| } | |
| # scan_venv LABEL VENV_ROOT | |
| # Given a venv root, locates its pip, runs the litellm check, and checks | |
| # site-packages for the .pth artifact. | |
| scan_venv() { | |
| local label="$1" | |
| local venv_root="$2" | |
| # Support both Unix (bin/) and Windows (Scripts/) layouts | |
| local pip_bin="" | |
| for candidate in "${venv_root}/bin/pip3" "${venv_root}/bin/pip" \ | |
| "${venv_root}/Scripts/pip3.exe" "${venv_root}/Scripts/pip.exe"; do | |
| if [[ -x "$candidate" ]]; then | |
| pip_bin="$candidate" | |
| break | |
| fi | |
| done | |
| [[ -z "$pip_bin" ]] && return # no pip found → not a usable venv | |
| check_litellm_in_pip "$label" "$pip_bin" | |
| # Also scan site-packages for .pth payload | |
| for sp in "${venv_root}"/lib/python*/site-packages; do | |
| [[ -d "$sp" ]] && check_pth_artifact "$sp" | |
| done | |
| } | |
| # ───────────────────────────────────────────────────────────────────────────── | |
| # Header | |
| # ───────────────────────────────────────────────────────────────────────────── | |
| #TOTAL_SECTIONS=$(($SCAN_WORKSPACE ? 3 : 2)) | |
| TOTAL_SECTIONS=2 | |
| if [ "$SCAN_WORKSPACE" = true ]; then | |
| TOTAL_SECTIONS=3 | |
| fi | |
| echo "" | |
| echo -e "${BLD}╔══════════════════════════════════════════════════════╗${RST}" | |
| echo -e "${BLD}║ litellm Compromise Detection Script ║${RST}" | |
| echo -e "${BLD}║ Incident: TeamPCP supply-chain — 24 Mar 2026 ║${RST}" | |
| echo -e "${BLD}╚══════════════════════════════════════════════════════╝${RST}" | |
| echo -e " ${BLD}Compromised versions : ${COMPROMISED_VERSIONS[*]}${RST}" | |
| if $SCAN_WORKSPACE; then | |
| echo -e " ${BLD}Mode : System check + workspace scan${RST}" | |
| echo -e " ${BLD}Workspace : ${WORKSPACE_DIR}${RST}" | |
| else | |
| echo -e " ${BLD}Mode : System check only${RST}" | |
| section_note "Tip: pass a directory path to also scan projects → $0 /your/workspace" | |
| fi | |
| # ══════════════════════════════════════════════════════════════════════════════ | |
| # SECTION 1 · System Python / pip / pyenv / conda / pipx / uv | |
| # ══════════════════════════════════════════════════════════════════════════════ | |
| banner "1 / ${TOTAL_SECTIONS} · System Python installations" | |
| declare -A seen_pips # deduplicates by resolved pip path | |
| # --- PATH-visible pythons ---------------------------------------------------- | |
| for py_cmd in python python3 python3.9 python3.10 python3.11 python3.12 python3.13; do | |
| command -v "$py_cmd" &>/dev/null 2>&1 || continue | |
| py_path=$(command -v "$py_cmd") | |
| pip_path="$(dirname "$py_path")/pip3" | |
| [[ ! -x "$pip_path" ]] && pip_path="$(dirname "$py_path")/pip" | |
| [[ ! -x "$pip_path" ]] && continue | |
| real_pip=$(realpath "$pip_path" 2>/dev/null || echo "$pip_path") | |
| [[ -n "${seen_pips[$real_pip]+_}" ]] && continue | |
| seen_pips[$real_pip]=1 | |
| site=$("$py_cmd" -c "import site; print(site.getsitepackages()[0])" 2>/dev/null || true) | |
| echo -e "\n${BLD}▶ ${py_cmd} → ${pip_path}${RST}" | |
| check_litellm_in_pip "system:${py_cmd}" "$pip_path" | |
| [[ -n "$site" ]] && check_pth_artifact "$site" | |
| done | |
| # --- Homebrew Python (macOS) ------------------------------------------------- | |
| for brew_py in /opt/homebrew/bin/python3 /usr/local/bin/python3; do | |
| [[ -x "$brew_py" ]] || continue | |
| brew_pip="${brew_py%python3}pip3" | |
| [[ -x "$brew_pip" ]] || continue | |
| real_pip=$(realpath "$brew_pip" 2>/dev/null || echo "$brew_pip") | |
| [[ -n "${seen_pips[$real_pip]+_}" ]] && continue | |
| seen_pips[$real_pip]=1 | |
| site=$("$brew_py" -c "import site; print(site.getsitepackages()[0])" 2>/dev/null || true) | |
| echo -e "\n${BLD}▶ Homebrew Python → ${brew_pip}${RST}" | |
| check_litellm_in_pip "homebrew:python3" "$brew_pip" | |
| [[ -n "$site" ]] && check_pth_artifact "$site" | |
| done | |
| # --- pyenv ------------------------------------------------------------------- | |
| PYENV_ROOT="${PYENV_ROOT:-$HOME/.pyenv}" | |
| if [[ -d "$PYENV_ROOT/versions" ]]; then | |
| echo -e "\n${BLD}▶ pyenv versions under ${PYENV_ROOT}/versions${RST}" | |
| while IFS= read -r pip_bin; do | |
| real_pip=$(realpath "$pip_bin" 2>/dev/null || echo "$pip_bin") | |
| [[ -n "${seen_pips[$real_pip]+_}" ]] && continue | |
| seen_pips[$real_pip]=1 | |
| py_ver=$(echo "$pip_bin" | awk -F'/' '{ | |
| for(i=1;i<=NF;i++) if($i=="versions") {print $(i+1); exit} | |
| }') | |
| echo -e " ${BLD}pyenv:${py_ver} → ${pip_bin}${RST}" | |
| check_litellm_in_pip "pyenv:${py_ver}" "$pip_bin" | |
| for sp in "$(dirname "$(dirname "$pip_bin")")"/lib/python*/site-packages; do | |
| [[ -d "$sp" ]] && check_pth_artifact "$sp" | |
| done | |
| done < <(find "$PYENV_ROOT/versions" \( -name "pip3" -o -name "pip" \) 2>/dev/null | sort -u) | |
| else | |
| section_note "pyenv not found at ${PYENV_ROOT}" | |
| fi | |
| # --- conda / mamba ----------------------------------------------------------- | |
| if command -v conda &>/dev/null 2>&1; then | |
| echo -e "\n${BLD}▶ conda environments${RST}" | |
| while IFS= read -r env_path; do | |
| [[ -z "$env_path" || "$env_path" == "#"* ]] && continue | |
| pip_bin="${env_path}/bin/pip3" | |
| [[ ! -x "$pip_bin" ]] && pip_bin="${env_path}/bin/pip" | |
| [[ ! -x "$pip_bin" ]] && continue | |
| real_pip=$(realpath "$pip_bin" 2>/dev/null || echo "$pip_bin") | |
| [[ -n "${seen_pips[$real_pip]+_}" ]] && continue | |
| seen_pips[$real_pip]=1 | |
| env_name=$(basename "$env_path") | |
| echo -e " ${BLD}conda env: ${env_name} → ${pip_bin}${RST}" | |
| check_litellm_in_pip "conda:${env_name}" "$pip_bin" | |
| for sp in "${env_path}"/lib/python*/site-packages; do | |
| [[ -d "$sp" ]] && check_pth_artifact "$sp" | |
| done | |
| done < <(conda env list 2>/dev/null | awk 'NF && !/^#/ {print $NF}') | |
| fi | |
| # --- pipx -------------------------------------------------------------------- | |
| if command -v pipx &>/dev/null 2>&1; then | |
| echo -e "\n${BLD}▶ pipx environment${RST}" | |
| pipx_litellm=$(pipx list 2>/dev/null | grep -i "litellm" || true) | |
| if [[ -n "$pipx_litellm" ]]; then | |
| FOUND_LITELLM_LOCATIONS+=("pipx → ${pipx_litellm}") | |
| echo -e " ${YLW}litellm found in pipx:${RST} ${pipx_litellm}" | |
| for cv in "${COMPROMISED_VERSIONS[@]}"; do | |
| if echo "$pipx_litellm" | grep -q "$cv"; then | |
| FOUND_COMPROMISED=1 | |
| COMPROMISED_LOCATIONS+=("pipx → ${pipx_litellm}") | |
| echo -e " ${RED}${BLD}✘ COMPROMISED version detected in pipx!${RST}" | |
| fi | |
| done | |
| else | |
| echo -e " ${GRN}✔ litellm not found in pipx${RST}" | |
| fi | |
| fi | |
| # --- uv cache (stale .pth wheels) ------------------------------------------- | |
| echo -e "\n${BLD}▶ uv cache — stale litellm_init.pth check${RST}" | |
| UV_CACHE="${HOME}/.cache/uv" | |
| if [[ -d "$UV_CACHE" ]]; then | |
| pth_hits=$(find "$UV_CACHE" -name "litellm_init.pth" 2>/dev/null || true) | |
| if [[ -n "$pth_hits" ]]; then | |
| FOUND_COMPROMISED=1 | |
| echo -e " ${RED}${BLD}✘ litellm_init.pth in uv cache — run: uv cache clean${RST}" | |
| while IFS= read -r f; do | |
| echo -e " ${RED}${f}${RST}" | |
| COMPROMISED_LOCATIONS+=("uv cache artifact: $f") | |
| done <<< "$pth_hits" | |
| else | |
| echo -e " ${GRN}✔ No litellm_init.pth in uv cache${RST}" | |
| fi | |
| else | |
| section_note "uv cache not found at ${UV_CACHE}" | |
| fi | |
| # ══════════════════════════════════════════════════════════════════════════════ | |
| # SECTION 2 (optional) · Workspace recursive scan | |
| # ══════════════════════════════════════════════════════════════════════════════ | |
| if $SCAN_WORKSPACE; then | |
| banner "2 / ${TOTAL_SECTIONS} · Workspace scan: ${WORKSPACE_DIR}" | |
| if [[ ! -d "$WORKSPACE_DIR" ]]; then | |
| echo -e " ${RED}✘ Directory not found: ${WORKSPACE_DIR}${RST}" | |
| else | |
| # ── 2a. Virtual environments (any depth, any name) ──────────────────────── | |
| # Strategy: locate every pyvenv.cfg — the canonical marker of any Python | |
| # venv regardless of folder name (venv / .venv / env / myenv / .dev / etc.) | |
| echo -e "${BLD}▶ 2a · Virtual environments (detected via pyvenv.cfg)${RST}" | |
| section_note "Catches venv / .venv / env / custom-named envs at any folder depth" | |
| venv_count=0 | |
| declare -A seen_venvs | |
| while IFS= read -r pyvenv_cfg; do | |
| venv_root=$(dirname "$pyvenv_cfg") | |
| real_venv=$(realpath "$venv_root" 2>/dev/null || echo "$venv_root") | |
| # Deduplicate venv roots | |
| [[ -n "${seen_venvs[$real_venv]+_}" ]] && continue | |
| seen_venvs[$real_venv]=1 | |
| # Skip if this pip was already seen in the system scan | |
| pip_candidate="${venv_root}/bin/pip3" | |
| [[ ! -x "$pip_candidate" ]] && pip_candidate="${venv_root}/bin/pip" | |
| if [[ -x "$pip_candidate" ]]; then | |
| real_pip=$(realpath "$pip_candidate" 2>/dev/null || echo "$pip_candidate") | |
| [[ -n "${seen_pips[$real_pip]+_}" ]] && continue | |
| seen_pips[$real_pip]=1 | |
| fi | |
| venv_count=$((venv_count + 1)) | |
| rel_path="${venv_root#"$WORKSPACE_DIR"/}" | |
| project_dir=$(dirname "$venv_root") | |
| echo -e "\n ${BLD}[${venv_count}] ${rel_path}${RST} ${DIM}← $(basename "$project_dir")${RST}" | |
| scan_venv "workspace:${rel_path}" "$venv_root" | |
| done < <(find "$WORKSPACE_DIR" \ | |
| -name "pyvenv.cfg" \ | |
| ! -path "*/.git/*" \ | |
| ! -path "*/node_modules/*" \ | |
| ! -path "*/__pycache__/*" \ | |
| 2>/dev/null | sort) | |
| echo "" | |
| echo -e " ${BLD}Total virtual environments found: ${venv_count}${RST}" | |
| # ── 2b. Bare installs — python/pip symlinks outside any venv ───────────── | |
| # Catches projects that install into a shared Python with no venv | |
| # (e.g. poetry --no-venv, scripts using system pip directly). | |
| echo "" | |
| echo -e "${BLD}▶ 2b · Bare Python installs (pip/python outside a venv)${RST}" | |
| section_note "Looks for pip/python binaries not inside a pyvenv.cfg-marked venv" | |
| bare_count=0 | |
| while IFS= read -r pip_bin; do | |
| # Skip if the parent directory chain contains a pyvenv.cfg (it IS a venv) | |
| venv_check=$(dirname "$(dirname "$pip_bin")") | |
| [[ -f "${venv_check}/pyvenv.cfg" ]] && continue | |
| real_pip=$(realpath "$pip_bin" 2>/dev/null || echo "$pip_bin") | |
| [[ -n "${seen_pips[$real_pip]+_}" ]] && continue | |
| seen_pips[$real_pip]=1 | |
| bare_count=$((bare_count + 1)) | |
| rel_path="${pip_bin#"$WORKSPACE_DIR"/}" | |
| echo -e " ${BLD}${rel_path}${RST}" | |
| check_litellm_in_pip "workspace-bare:${rel_path}" "$pip_bin" | |
| done < <(find "$WORKSPACE_DIR" \ | |
| \( -name "pip3" -o -name "pip" \) \ | |
| -type f \ | |
| ! -path "*/.git/*" \ | |
| ! -path "*/node_modules/*" \ | |
| 2>/dev/null | sort) | |
| [[ $bare_count -eq 0 ]] && section_note "No bare pip binaries found outside venvs" | |
| # ── 2c. Dependency file scan ────────────────────────────────────────────── | |
| echo "" | |
| echo -e "${BLD}▶ 2c · Dependency files — pinned version check${RST}" | |
| section_note "Scans: requirements*.txt, pyproject.toml, setup.cfg, setup.py," | |
| section_note " Pipfile, Pipfile.lock, poetry.lock, uv.lock" | |
| dep_file_count=0 | |
| compromised_dep_hits=0 | |
| while IFS= read -r dep_file; do | |
| dep_file_count=$((dep_file_count + 1)) | |
| rel_dep="${dep_file#"$WORKSPACE_DIR"/}" | |
| file_flagged=false | |
| # Check for pinned compromised version | |
| for cv in "${COMPROMISED_VERSIONS[@]}"; do | |
| if grep -qiE "litellm[^a-zA-Z0-9_-].*${cv}|litellm==${cv}" "$dep_file" 2>/dev/null; then | |
| if ! $file_flagged; then | |
| echo -e " ${RED}${BLD}✘ Compromised version pinned: ${rel_dep}${RST}" | |
| file_flagged=true | |
| compromised_dep_hits=$((compromised_dep_hits + 1)) | |
| fi | |
| grep -niE "litellm[^a-zA-Z0-9_-].*${cv}|litellm==${cv}" "$dep_file" \ | |
| | while read -r match; do echo -e " ${RED}line ${match}${RST}"; done | |
| fi | |
| done | |
| # Non-compromised litellm reference (informational) | |
| if ! $file_flagged && grep -qiE "litellm" "$dep_file" 2>/dev/null; then | |
| first_match=$(grep -iE "litellm" "$dep_file" | head -1 | sed 's/^[[:space:]]*//') | |
| echo -e " ${YLW}⚠ litellm reference: ${rel_dep}${RST}" | |
| echo -e " ${DIM}→ ${first_match}${RST}" | |
| fi | |
| done < <(find "$WORKSPACE_DIR" \ | |
| \( -name "requirements*.txt" \ | |
| -o -name "pyproject.toml" \ | |
| -o -name "setup.cfg" \ | |
| -o -name "setup.py" \ | |
| -o -name "Pipfile" \ | |
| -o -name "Pipfile.lock" \ | |
| -o -name "poetry.lock" \ | |
| -o -name "uv.lock" \) \ | |
| ! -path "*/.git/*" \ | |
| ! -path "*/node_modules/*" \ | |
| ! -path "*/__pycache__/*" \ | |
| 2>/dev/null | sort) | |
| echo "" | |
| echo -e " ${DIM}Dependency files scanned: ${dep_file_count}${RST}" | |
| if [[ $compromised_dep_hits -eq 0 ]]; then | |
| echo -e " ${GRN}✔ No pinned compromised versions in dependency files${RST}" | |
| else | |
| echo -e " ${RED}${BLD}✘ ${compromised_dep_hits} file(s) pin a compromised version${RST}" | |
| fi | |
| fi # end: workspace dir exists | |
| fi # end: SCAN_WORKSPACE | |
| # ══════════════════════════════════════════════════════════════════════════════ | |
| # SECTION (last) · Malware persistence artifacts | |
| # ══════════════════════════════════════════════════════════════════════════════ | |
| banner "${TOTAL_SECTIONS} / ${TOTAL_SECTIONS} · Malware persistence artifacts" | |
| # --- sysmon backdoor --------------------------------------------------------- | |
| echo -e "${BLD}▶ sysmon backdoor files${RST}" | |
| SYSMON_PY="${HOME}/.config/sysmon/sysmon.py" | |
| SYSMON_SERVICE="${HOME}/.config/systemd/user/sysmon.service" | |
| sysmon_clean=true | |
| if [[ -f "$SYSMON_PY" ]]; then | |
| sysmon_clean=false | |
| FOUND_COMPROMISED=1 | |
| COMPROMISED_LOCATIONS+=("Backdoor script: ${SYSMON_PY}") | |
| echo -e " ${RED}${BLD}✘ BACKDOOR FOUND: ${SYSMON_PY}${RST}" | |
| echo -e " ${RED} Rotate ALL credentials, then: rm -rf ~/.config/sysmon${RST}" | |
| fi | |
| if [[ -f "$SYSMON_SERVICE" ]]; then | |
| sysmon_clean=false | |
| FOUND_COMPROMISED=1 | |
| COMPROMISED_LOCATIONS+=("Backdoor service: ${SYSMON_SERVICE}") | |
| echo -e " ${RED}${BLD}✘ SYSTEMD PERSISTENCE: ${SYSMON_SERVICE}${RST}" | |
| echo -e " ${RED} Disable with: systemctl --user disable --now sysmon${RST}" | |
| fi | |
| $sysmon_clean && echo -e " ${GRN}✔ No sysmon backdoor found${RST}" | |
| # --- litellm_init.pth (global filesystem) ------------------------------------ | |
| echo "" | |
| echo -e "${BLD}▶ litellm_init.pth global search${RST}" | |
| search_roots=("/usr" "$HOME") | |
| [[ -d "/opt/homebrew" ]] && search_roots+=("/opt/homebrew") | |
| [[ -d "/usr/local" ]] && search_roots+=("/usr/local") | |
| $SCAN_WORKSPACE && [[ -d "$WORKSPACE_DIR" ]] && search_roots+=("$WORKSPACE_DIR") | |
| pth_hits_global=$(find "${search_roots[@]}" \ | |
| -name "litellm_init.pth" \ | |
| ! -path "*/.Trash/*" \ | |
| 2>/dev/null | sort || true) | |
| if [[ -n "$pth_hits_global" ]]; then | |
| FOUND_COMPROMISED=1 | |
| echo -e " ${RED}${BLD}✘ litellm_init.pth detected (auto-exec payload):${RST}" | |
| while IFS= read -r f; do | |
| echo -e " ${RED}${f}${RST}" | |
| COMPROMISED_LOCATIONS+=("Malware .pth: $f") | |
| done <<< "$pth_hits_global" | |
| else | |
| echo -e " ${GRN}✔ No litellm_init.pth found${RST}" | |
| fi | |
| # --- Active C2 connection ---------------------------------------------------- | |
| echo "" | |
| echo -e "${BLD}▶ Live connection to C2 (models.litellm.cloud)${RST}" | |
| c2_hits="" | |
| if command -v lsof &>/dev/null 2>&1; then | |
| c2_hits=$(lsof -i -n -P 2>/dev/null | grep -i "litellm.cloud" || true) | |
| elif command -v ss &>/dev/null 2>&1; then | |
| c2_hits=$(ss -tnp 2>/dev/null | grep -i "litellm.cloud" || true) | |
| else | |
| echo -e " ${YLW}⚠ Neither lsof nor ss available — skipping live connection check${RST}" | |
| fi | |
| if [[ -n "$c2_hits" ]]; then | |
| FOUND_COMPROMISED=1 | |
| COMPROMISED_LOCATIONS+=("Active C2 connection detected") | |
| echo -e " ${RED}${BLD}✘ ACTIVE C2 CONNECTION:${RST}" | |
| while IFS= read -r line; do echo -e " ${RED}${line}${RST}"; done <<< "$c2_hits" | |
| elif [[ -n "$c2_hits+x" ]]; then | |
| echo -e " ${GRN}✔ No active connection to models.litellm.cloud${RST}" | |
| fi | |
| # ══════════════════════════════════════════════════════════════════════════════ | |
| # FINAL SUMMARY | |
| # ══════════════════════════════════════════════════════════════════════════════ | |
| banner "SUMMARY REPORT" | |
| if [[ ${#FOUND_LITELLM_LOCATIONS[@]} -eq 0 ]]; then | |
| echo -e "${GRN}${BLD}✔ litellm is NOT installed anywhere checked.${RST}" | |
| else | |
| echo -e "${BLD}All litellm installations found (${#FOUND_LITELLM_LOCATIONS[@]}):${RST}" | |
| for loc in "${FOUND_LITELLM_LOCATIONS[@]}"; do | |
| echo -e " • ${loc}" | |
| done | |
| fi | |
| echo "" | |
| if [[ $FOUND_COMPROMISED -eq 1 ]]; then | |
| echo -e "${RED}${BLD}╔══════════════════════════════════════════════════════╗${RST}" | |
| echo -e "${RED}${BLD}║ ⚠ COMPROMISE DETECTED — IMMEDIATE ACTION REQUIRED ║${RST}" | |
| echo -e "${RED}${BLD}╚══════════════════════════════════════════════════════╝${RST}" | |
| echo "" | |
| echo -e "${RED}${BLD}Affected items:${RST}" | |
| for loc in "${COMPROMISED_LOCATIONS[@]}"; do | |
| echo -e " ${RED}✘ ${loc}${RST}" | |
| done | |
| echo "" | |
| echo -e "${BLD}REMEDIATION STEPS:${RST}" | |
| echo -e " 1. In EVERY affected environment:" | |
| echo -e " ${RED}pip uninstall litellm -y${RST}" | |
| echo -e " ${RED}pip install 'litellm>=1.82.9'${RST} (first clean version)" | |
| echo -e " 2. Purge package caches:" | |
| echo -e " ${RED}pip cache purge${RST} && ${RED}uv cache clean${RST}" | |
| echo -e " 3. Remove malware files (if found):" | |
| echo -e " ${RED}rm -rf ~/.config/sysmon${RST}" | |
| echo -e " ${RED}systemctl --user disable --now sysmon${RST} (Linux)" | |
| echo -e " 4. ${BLD}ROTATE ALL CREDENTIALS IMMEDIATELY:${RST}" | |
| echo -e " • AWS / GCP / Azure / cloud provider keys" | |
| echo -e " • SSH private keys (~/.ssh/id_*)" | |
| echo -e " • API keys in .env / config files" | |
| echo -e " • GitHub / GitLab tokens" | |
| echo -e " • Crypto wallet seed phrases" | |
| echo -e " • Kubernetes service-account tokens" | |
| echo -e " 5. Audit router/firewall logs for POSTs to models.litellm.cloud" | |
| echo -e " 6. Kubernetes: inspect kube-system for pods matching 'node-setup-*'" | |
| echo "" | |
| echo -e " ${YLW}⚠ Do NOT simply upgrade litellm — the payload may already have run.${RST}" | |
| echo -e " ${YLW} Consider rebuilding affected systems from a known-clean snapshot.${RST}" | |
| else | |
| echo -e "${GRN}${BLD}╔══════════════════════════════════════════════════╗${RST}" | |
| echo -e "${GRN}${BLD}║ ✔ No compromised litellm versions detected ║${RST}" | |
| echo -e "${GRN}${BLD}╚══════════════════════════════════════════════════╝${RST}" | |
| echo "" | |
| echo -e " ${DIM}Checked versions : ${COMPROMISED_VERSIONS[*]}${RST}" | |
| echo -e " ${DIM}Checked : system Python, Homebrew, pyenv, conda, pipx, uv cache${RST}" | |
| if $SCAN_WORKSPACE; then | |
| echo -e " ${DIM} all venvs + bare installs + dep files under:${RST}" | |
| echo -e " ${DIM} ${WORKSPACE_DIR}${RST}" | |
| fi | |
| echo -e " ${DIM}Artifacts checked : litellm_init.pth, sysmon backdoor, C2 connections${RST}" | |
| fi | |
| echo "" | |
| echo -e "${CYN}${DIM}Incident ref : https://docs.litellm.ai/blog/security-update-march-2026${RST}" | |
| echo -e "${CYN}${DIM}SNYK ID : SNYK-PYTHON-LITELLM-15762713${RST}" | |
| echo "" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment