Skip to content

Instantly share code, notes, and snippets.

@oneryalcin
Last active March 24, 2026 23:42
Show Gist options
  • Select an option

  • Save oneryalcin/c9e624278221503cab6045b1877348ca to your computer and use it in GitHub Desktop.

Select an option

Save oneryalcin/c9e624278221503cab6045b1877348ca to your computer and use it in GitHub Desktop.
litellm supply-chain attack auditor for macOS workstations
#!/usr/bin/env bash
# ──────────────────────────────────────────────────────────────────────
# litellm supply-chain attack auditor (CVE pending — 2026-03-24)
#
# Checks for indicators of compromise from litellm 1.82.7 / 1.82.8
# which were published to PyPI with a malicious .pth payload that
# steals credentials, exfiltrates to models.litellm.cloud, and
# attempts lateral movement via Kubernetes.
#
# Safe versions: anything < 1.82.7
# Compromised: 1.82.7, 1.82.8 (and potentially later)
#
# Usage: chmod +x audit_litellm_attack.sh && ./audit_litellm_attack.sh
# Or: bash audit_litellm_attack.sh
# Recommended:
# 1. Run --quick first for a fast triage pass on developer workstations.
# 2. Run --deep only if --quick returns warnings/unsafe findings or you have
# specific reason to suspect compromise.
# 3. Treat "ALL CLEAR" as "no obvious indicators found", not proof of safety.
# ──────────────────────────────────────────────────────────────────────
set -euo pipefail
MODE="${AUDIT_MODE:-quick}"
JSON_OUTPUT=0
MAX_IMAGE_AUDIT="${AUDIT_MAX_IMAGE_AUDIT:-5}"
LOCKFILE_MAX_DEPTH=5
CURRENT_SECTION=""
usage() {
cat <<'EOF'
Usage: audit_litellm_attack.sh [--quick|--deep] [--json] [--scan-root PATH]
Options:
--quick Fast default scan focused on high-signal paths
--deep Broader scan that also walks the home directory
--json Emit machine-readable JSON summary
--scan-root PATH Add an extra project/workspace root to scan
--help Show this help text
EOF
}
declare -a EXTRA_SCAN_ROOTS=()
while [[ $# -gt 0 ]]; do
case "$1" in
--quick)
MODE="quick"
shift
;;
--deep)
MODE="deep"
shift
;;
--json)
JSON_OUTPUT=1
shift
;;
--scan-root)
[[ $# -lt 2 ]] && { echo "Missing value for --scan-root" >&2; exit 1; }
EXTRA_SCAN_ROOTS+=("$2")
shift 2
;;
--help|-h)
usage
exit 0
;;
*)
echo "Unknown argument: $1" >&2
usage >&2
exit 1
;;
esac
done
if [[ "$MODE" == "deep" ]]; then
MAX_IMAGE_AUDIT="${AUDIT_MAX_IMAGE_AUDIT:-20}"
LOCKFILE_MAX_DEPTH=8
fi
if [[ $JSON_OUTPUT -eq 1 ]]; then
exec 3>&1
exec 1>/dev/null
fi
# ── colours ──────────────────────────────────────────────────────────
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
CYAN='\033[0;36m'
BOLD='\033[1m'
DIM='\033[2m'
RESET='\033[0m'
UNSAFE=0
WARN=0
FINDINGS_FILE="$(mktemp "${TMPDIR:-/tmp}/litellm-audit-findings.XXXXXX")"
SEEN_LOCKS_FILE="$(mktemp "${TMPDIR:-/tmp}/litellm-audit-seen-locks.XXXXXX")"
SEEN_INSTALLS_FILE="$(mktemp "${TMPDIR:-/tmp}/litellm-audit-seen-installs.XXXXXX")"
cleanup() {
rm -f "$FINDINGS_FILE"
rm -f "$SEEN_LOCKS_FILE"
rm -f "$SEEN_INSTALLS_FILE"
}
trap cleanup EXIT
record_finding() {
printf '%s\t%s\t%s\t%s\n' "$CURRENT_SECTION" "$1" "$2" "$3" >> "$FINDINGS_FILE"
}
header() { CURRENT_SECTION="$1"; if [[ $JSON_OUTPUT -eq 0 ]]; then printf "\n${BOLD}${CYAN}━━━ %s ━━━${RESET}\n" "$1"; fi; return 0; }
safe() { record_finding "low" "ok" "$1"; if [[ $JSON_OUTPUT -eq 0 ]]; then printf " ${GREEN}[SAFE]${RESET} %s\n" "$1"; fi; return 0; }
unsafe() { UNSAFE=$((UNSAFE + 1)); record_finding "critical" "unsafe" "$1"; if [[ $JSON_OUTPUT -eq 0 ]]; then printf " ${RED}[UNSAFE]${RESET} %s\n" "$1"; fi; return 0; }
warning() { WARN=$((WARN + 1)); record_finding "medium" "warning" "$1"; if [[ $JSON_OUTPUT -eq 0 ]]; then printf " ${YELLOW}[WARNING]${RESET} %s\n" "$1"; fi; return 0; }
info() { record_finding "info" "info" "$1"; if [[ $JSON_OUTPUT -eq 0 ]]; then printf " ${DIM}[INFO]${RESET} %s\n" "$1"; fi; return 0; }
skip() { record_finding "info" "skipped" "$1"; if [[ $JSON_OUTPUT -eq 0 ]]; then printf " ${DIM}[SKIP]${RESET} %s\n" "$1"; fi; return 0; }
permission_limited() { record_finding "info" "permission_limited" "$1"; if [[ $JSON_OUTPUT -eq 0 ]]; then printf " ${DIM}[LIMITED]${RESET} %s\n" "$1"; fi; return 0; }
# Known-bad versions (add more if the attack expands)
BAD_VERSIONS="1.82.7|1.82.8"
# Configurable scan roots for workstation-wide auditing.
# AUDIT_SCAN_ROOTS may be colon-separated or newline-separated.
declare -a SCAN_ROOTS=()
if [[ -n "${AUDIT_SCAN_ROOTS:-}" ]]; then
while IFS= read -r scan_root; do
[[ -n "$scan_root" ]] && SCAN_ROOTS+=("$scan_root")
done < <(printf '%s\n' "$AUDIT_SCAN_ROOTS" | tr ':' '\n')
else
SCAN_ROOTS=("$PWD" "$HOME/dev" "$HOME/src" "$HOME/projects" "$HOME/code" "$HOME/work")
fi
for extra_root in "${EXTRA_SCAN_ROOTS[@]+"${EXTRA_SCAN_ROOTS[@]}"}"; do
SCAN_ROOTS+=("$extra_root")
done
[[ "$MODE" == "deep" ]] && SCAN_ROOTS+=("$HOME")
deduped_scan_roots=()
while IFS= read -r scan_root; do
deduped_scan_roots+=("$scan_root")
done < <(printf '%s\n' "${SCAN_ROOTS[@]}" | awk 'NF && !seen[$0]++')
SCAN_ROOTS=("${deduped_scan_roots[@]}")
SELF_PATH="$(cd "$(dirname "$0")" && pwd -P)/$(basename "$0")"
COMMON_PRUNE_ARGS=(
-path "*/.git" -prune -o
-path "*/node_modules" -prune -o
-path "*/.mypy_cache" -prune -o
-path "*/.pytest_cache" -prune -o
)
if [[ "$MODE" == "deep" ]]; then
COMMON_PRUNE_ARGS+=(
-path "*/Library" -prune -o
-path "*/Downloads" -prune -o
-path "*/Pictures" -prune -o
-path "*/Music" -prune -o
-path "*/Movies" -prune -o
-path "*/Public" -prune -o
)
fi
INSTALL_PRUNE_ARGS=("${COMMON_PRUNE_ARGS[@]}")
BROAD_PRUNE_ARGS=("${COMMON_PRUNE_ARGS[@]}" -path "*/.venv" -prune -o)
# ── helpers ──────────────────────────────────────────────────────────
is_bad_version() {
echo "$1" | grep -qE "^($BAD_VERSIONS)$"
}
extract_version_from_path() {
echo "$1" | grep -oE 'litellm[-_](([0-9]+\.)+[0-9]+)' | head -1 | sed 's/litellm[-_]//'
}
dedupe_paths() {
awk 'NF && !seen[$0]++'
}
parse_lockfile_versions() {
local lockfile="$1"
case "$(basename "$lockfile")" in
uv.lock|poetry.lock)
awk '
$0 ~ /^name = "litellm"$/ { in_litellm = 1; next }
in_litellm && $0 ~ /^version = "/ {
gsub(/^version = "/, "", $0)
gsub(/"$/, "", $0)
print $0
in_litellm = 0
}
$0 ~ /^\[\[/ || $0 ~ /^\[/ { in_litellm = 0 }
' "$lockfile" 2>/dev/null | dedupe_paths
;;
Pipfile.lock)
grep -oE '"litellm"[[:space:]]*:[[:space:]]*\{[^}]*"version"[[:space:]]*:[[:space:]]*"==?([0-9]+\.)+[0-9]+"' "$lockfile" 2>/dev/null \
| grep -oE '([0-9]+\.)+[0-9]+' \
| dedupe_paths
;;
requirements*.txt)
grep -oE '^litellm([[:space:]]*\[[^]]+\])?[[:space:]]*([=><!~]=)[[:space:]]*([0-9]+\.)+[0-9]+' "$lockfile" 2>/dev/null \
| grep -oE '([0-9]+\.)+[0-9]+' \
| dedupe_paths
;;
*)
grep -oE 'litellm[=_ -]+(([0-9]+\.)+[0-9]+)' "$lockfile" 2>/dev/null \
| grep -oE '([0-9]+\.)+[0-9]+' \
| dedupe_paths
;;
esac
return 0
}
# Run a command with a timeout (macOS compatible)
run_with_timeout() {
local timeout_sec="$1"; shift
local output
if command -v timeout &>/dev/null; then
output=$(timeout "$timeout_sec" "$@" 2>/dev/null) || true
else
output=$(perl -e 'alarm shift; exec @ARGV' "$timeout_sec" "$@" 2>/dev/null) || true
fi
echo "$output"
}
run_shell_with_timeout() {
local timeout_sec="$1"; shift
local command_str="$1"
local output
if command -v timeout &>/dev/null; then
output=$(timeout "$timeout_sec" /bin/bash -lc "$command_str" 2>/dev/null) || true
else
output=$(perl -e 'alarm shift; exec @ARGV' "$timeout_sec" /bin/bash -lc "$command_str" 2>/dev/null) || true
fi
echo "$output"
}
report_sha256() {
local target="$1"
local digest
[[ ! -f "$target" ]] && return 0
if command -v shasum &>/dev/null; then
digest=$(shasum -a 256 "$target" 2>/dev/null | awk '{print $1}' || true)
[[ -n "$digest" ]] && info "SHA256 $(basename "$target"): $digest"
fi
return 0
}
proxy_payload_ioc() {
local target="$1"
grep -qE 'b64_payload|base64\.b64decode|subprocess\.(run|Popen)\(\[sys\.executable|PERSIST_B64|checkmarx\.zone/raw' "$target" 2>/dev/null
}
pth_payload_ioc() {
local target="$1"
[[ ! -f "$target" ]] && return 1
grep -qE 'models\.litellm\.cloud|checkmarx\.zone/raw|PERSIST_B64|b64_payload|base64\.b64decode|subprocess\.(run|Popen)\(\[sys\.executable|session\.key\.enc|payload\.enc|tpcp\.tar\.gz|sysmon\.service|sysmon\.py' "$target" 2>/dev/null
}
inspect_installed_proxy_payload() {
local dist_info="$1"
local ver="$2"
local base_path proxy_path
base_path="${dist_info%.dist-info}"
base_path="${base_path%/litellm-*}"
proxy_path="$base_path/litellm/proxy/proxy_server.py"
[[ ! -f "$proxy_path" ]] && return 1
if proxy_payload_ioc "$proxy_path"; then
unsafe "Malicious proxy_server.py payload pattern found for litellm $ver: $proxy_path"
report_sha256 "$proxy_path"
return 0
fi
return 1
}
inspect_cache_proxy_payloads() {
local cache_root="$1"
find "$cache_root" \
-path "*/litellm/proxy/proxy_server.py" -type f -print 2>/dev/null \
| while IFS= read -r proxy_path; do
proxy_payload_ioc "$proxy_path" || continue
unsafe "Cached malicious proxy_server.py payload pattern found: $proxy_path"
report_sha256 "$proxy_path"
done
}
note_permission_limitations() {
if [[ "$MODE" == "deep" && -d "$HOME/Library" ]]; then
permission_limited "Deep mode prunes large macOS home directories such as Library/Downloads/Pictures to keep runtime bounded"
fi
}
# ── 1. Malicious .pth file (the primary payload) ────────────────────
header "1. Malicious .pth file scan"
note_permission_limitations
PTH_DIRS=(
"$HOME/.cache/uv"
"$HOME/.cache/pip"
"$HOME/Library/Caches/pypoetry"
"$HOME/Library/Caches/uv"
"$HOME/Library/Caches/pip"
"/usr/local/lib"
"$HOME/.local/lib"
)
# Add scan roots
for sr in "${SCAN_ROOTS[@]}"; do PTH_DIRS+=("$sr"); done
pth_found=0
suspicious_pth_found=0
for dir in "${PTH_DIRS[@]}"; do
if [[ -d "$dir" ]]; then
hits=$(find "$dir" -name "litellm_init.pth" -type f 2>/dev/null || true)
if [[ -n "$hits" ]]; then
while IFS= read -r hit; do
unsafe ".pth payload FOUND: $hit"
report_sha256 "$hit"
pth_found=1
done <<< "$hits"
fi
suspicious_pth_hits=$(find "$dir" -name "*.pth" -type f -print 2>/dev/null \
| while IFS= read -r pth_path; do
pth_payload_ioc "$pth_path" || continue
grep -HnE 'models\.litellm\.cloud|checkmarx\.zone/raw|PERSIST_B64|b64_payload|base64\.b64decode|subprocess\.(run|Popen)\(\[sys\.executable|session\.key\.enc|payload\.enc|tpcp\.tar\.gz|sysmon\.service|sysmon\.py' "$pth_path" 2>/dev/null
done | head -20 || true)
if [[ -n "$suspicious_pth_hits" ]]; then
warning "High-signal suspicious .pth file content found in $dir:"
echo "$suspicious_pth_hits" | while IFS= read -r line; do printf " %s\n" "$line"; done
while IFS=: read -r suspicious_path _; do
[[ -n "$suspicious_path" ]] && report_sha256 "$suspicious_path"
done <<< "$(echo "$suspicious_pth_hits" | cut -d: -f1 | dedupe_paths)"
suspicious_pth_found=1
fi
fi
done
# Also scan all site-packages on the system
site_pth=$(python3 -c "import site; print('\n'.join(site.getsitepackages()))" 2>/dev/null || true)
if [[ -n "$site_pth" ]]; then
while IFS= read -r sp; do
if [[ -d "$sp" ]]; then
hits=$(find "$sp" -name "litellm_init.pth" -type f 2>/dev/null || true)
if [[ -n "$hits" ]]; then
while IFS= read -r hit; do
unsafe ".pth payload in site-packages: $hit"
report_sha256 "$hit"
pth_found=1
done <<< "$hits"
fi
suspicious_site_pth=$(find "$sp" -maxdepth 2 -name "*.pth" -type f -print 2>/dev/null \
| while IFS= read -r pth_path; do
pth_payload_ioc "$pth_path" || continue
grep -HnE 'models\.litellm\.cloud|checkmarx\.zone/raw|PERSIST_B64|b64_payload|base64\.b64decode|subprocess\.(run|Popen)\(\[sys\.executable|session\.key\.enc|payload\.enc|tpcp\.tar\.gz|sysmon\.service|sysmon\.py' "$pth_path" 2>/dev/null
done | head -20 || true)
if [[ -n "$suspicious_site_pth" ]]; then
warning "High-signal suspicious .pth file content found in site-packages: $sp"
echo "$suspicious_site_pth" | while IFS= read -r line; do printf " %s\n" "$line"; done
while IFS= read -r suspicious_path; do
[[ -n "$suspicious_path" ]] && report_sha256 "$suspicious_path"
done <<< "$(echo "$suspicious_site_pth" | cut -d: -f1 | dedupe_paths)"
suspicious_pth_found=1
fi
fi
done <<< "$site_pth"
fi
if [[ $pth_found -eq 0 && $suspicious_pth_found -eq 0 ]]; then
safe "No litellm_init.pth found anywhere"
fi
# ── 2. Package manager caches ───────────────────────────────────────
header "2. Package manager caches"
# --- uv cache ---
uv_cache_dirs=("$HOME/.cache/uv" "$HOME/Library/Caches/uv")
uv_checked=0
for uv_cache in "${uv_cache_dirs[@]}"; do
if [[ -d "$uv_cache" ]]; then
uv_checked=1
# Check by filename (wheels dir uses version in path)
bad_in_uv=$(find "$uv_cache" \( -path "*litellm-1.82.7*" -o -path "*litellm-1.82.8*" -o -path "*litellm/1.82.7*" -o -path "*litellm/1.82.8*" \) 2>/dev/null || true)
# Check content-addressed archives by grepping METADATA files
# uv's archive-v0 uses hashed dirs, version only appears inside METADATA
bad_in_archive=$(find "$uv_cache/archive-v0" -path "*litellm*METADATA" -exec grep -l "^Version: 1\.82\.[78]" {} \; 2>/dev/null || true)
if [[ -n "$bad_in_uv" || -n "$bad_in_archive" ]]; then
unsafe "Compromised litellm found in uv cache ($uv_cache):"
{ echo "$bad_in_uv"; echo "$bad_in_archive"; } | grep -v '^$' | head -10 | while IFS= read -r f; do printf " %s\n" "$f"; done
else
cached_versions=$(find "$uv_cache" -path "*litellm*" \( -name "*.msgpack" -o -name "*.dist-info" \) 2>/dev/null \
| grep -oE 'litellm[-/](([0-9]+\.)+[0-9]+)' \
| sed 's/litellm[-/]//' \
| sort -uV 2>/dev/null || true)
# Also check archive METADATA for versions not visible in paths
archive_versions=$(find "$uv_cache/archive-v0" -path "*litellm*METADATA" -exec grep "^Version:" {} \; 2>/dev/null \
| sed 's/Version: //' | sort -uV 2>/dev/null || true)
all_versions=$(printf "%s\n%s" "$cached_versions" "$archive_versions" | grep -v '^$' | sort -uV 2>/dev/null || true)
if [[ -n "$all_versions" ]]; then
safe "uv cache ($uv_cache) — cached versions: $(echo $all_versions | tr '\n' ' ')"
else
info "uv cache ($uv_cache) — no litellm cached"
fi
fi
inspect_cache_proxy_payloads "$uv_cache"
fi
done
[[ $uv_checked -eq 0 ]] && skip "No uv cache found"
# --- poetry cache ---
poetry_cache="$HOME/Library/Caches/pypoetry"
if [[ -d "$poetry_cache" ]]; then
bad_in_poetry=$(find "$poetry_cache" \( -name "litellm-1.82.7*" -o -name "litellm-1.82.8*" \) 2>/dev/null || true)
if [[ -n "$bad_in_poetry" ]]; then
unsafe "Compromised litellm found in poetry cache:"
echo "$bad_in_poetry" | head -10 | while IFS= read -r f; do printf " %s\n" "$f"; done
else
cached_versions=$(find "$poetry_cache" -name "litellm-*.whl" 2>/dev/null \
| grep -oE 'litellm-(([0-9]+\.)+[0-9]+)' \
| sed 's/litellm-//' \
| sort -uV 2>/dev/null || true)
if [[ -n "$cached_versions" ]]; then
safe "poetry cache — cached versions: $(echo $cached_versions | tr '\n' ' ')"
else
info "poetry cache — no litellm cached"
fi
fi
inspect_cache_proxy_payloads "$poetry_cache"
else
skip "No poetry cache found"
fi
# --- pip cache ---
pip_cache_dirs=("$HOME/Library/Caches/pip" "$HOME/.cache/pip")
pip_checked=0
for pip_cache in "${pip_cache_dirs[@]}"; do
if [[ -d "$pip_cache" ]]; then
pip_checked=1
bad_in_pip=$(find "$pip_cache" \( -name "litellm-1.82.7*" -o -name "litellm-1.82.8*" \) 2>/dev/null || true)
if [[ -n "$bad_in_pip" ]]; then
unsafe "Compromised litellm found in pip cache ($pip_cache):"
echo "$bad_in_pip" | head -10 | while IFS= read -r f; do printf " %s\n" "$f"; done
else
cached_versions=$(find "$pip_cache" -name "litellm-*.whl" 2>/dev/null \
| grep -oE 'litellm-(([0-9]+\.)+[0-9]+)' \
| sed 's/litellm-//' \
| sort -uV 2>/dev/null || true)
if [[ -n "$cached_versions" ]]; then
safe "pip cache ($pip_cache) — cached versions: $(echo $cached_versions | tr '\n' ' ')"
else
info "pip cache ($pip_cache) — no litellm cached"
fi
fi
inspect_cache_proxy_payloads "$pip_cache"
fi
done
[[ $pip_checked -eq 0 ]] && skip "No pip cache found"
# ── 3. Installed packages (venvs, conda, global) ────────────────────
header "3. Installed litellm packages"
venv_found=0
# --- virtualenvs and arbitrary site-packages under workstation roots ---
for root in "${SCAN_ROOTS[@]}"; do
if [[ -d "$root" ]]; then
while IFS= read -r dist_info; do
[[ -z "$dist_info" ]] && continue
ver=$(extract_version_from_path "$dist_info")
venv_path=$(echo "$dist_info" | sed 's|/lib/.*||')
grep -Fqx "$venv_path|$ver" "$SEEN_INSTALLS_FILE" 2>/dev/null && continue
printf '%s|%s\n' "$venv_path" "$ver" >> "$SEEN_INSTALLS_FILE"
venv_found=$((venv_found + 1))
if is_bad_version "$ver"; then
unsafe "Compromised litellm $ver installed in: $venv_path"
else
safe "litellm $ver in: $venv_path"
fi
inspect_installed_proxy_payload "$dist_info" "$ver" || true
done < <(find "$root" \
"${INSTALL_PRUNE_ARGS[@]}" \
-path "*/site-packages/litellm-*.dist-info" -type d -print 2>/dev/null || true)
fi
done
# --- ~/.local/lib site-packages (user installs) ---
local_dist=$(find "$HOME/.local/lib" -path "*/site-packages/litellm-*.dist-info" -type d 2>/dev/null || true)
if [[ -n "$local_dist" ]]; then
while IFS= read -r dist_info; do
ver=$(extract_version_from_path "$dist_info")
grep -Fqx "$dist_info|$ver" "$SEEN_INSTALLS_FILE" 2>/dev/null && continue
printf '%s|%s\n' "$dist_info" "$ver" >> "$SEEN_INSTALLS_FILE"
venv_found=$((venv_found + 1))
if is_bad_version "$ver"; then
unsafe "Compromised litellm $ver in ~/.local: $dist_info"
else
safe "litellm $ver in ~/.local: $dist_info"
fi
inspect_installed_proxy_payload "$dist_info" "$ver" || true
done <<< "$local_dist"
fi
# --- active interpreter paths ---
site_paths=$(python3 - <<'PY' 2>/dev/null || true
import site
paths = []
getter = getattr(site, "getsitepackages", None)
if getter:
try:
paths.extend(getter())
except Exception:
pass
user_site = getattr(site, "getusersitepackages", lambda: None)()
if user_site:
paths.append(user_site)
for path in dict.fromkeys(paths):
print(path)
PY
)
if [[ -n "$site_paths" ]]; then
while IFS= read -r sp; do
[[ ! -d "$sp" ]] && continue
dist_info=$(find "$sp" -maxdepth 1 -name "litellm-*.dist-info" -type d 2>/dev/null | head -1 || true)
[[ -z "$dist_info" ]] && continue
ver=$(extract_version_from_path "$dist_info")
grep -Fqx "$sp|$ver" "$SEEN_INSTALLS_FILE" 2>/dev/null && continue
printf '%s|%s\n' "$sp" "$ver" >> "$SEEN_INSTALLS_FILE"
venv_found=$((venv_found + 1))
if is_bad_version "$ver"; then
unsafe "Compromised litellm $ver in interpreter site-packages: $sp"
else
safe "litellm $ver in interpreter site-packages: $sp"
fi
inspect_installed_proxy_payload "$dist_info" "$ver" || true
done <<< "$site_paths"
fi
# --- conda environments ---
conda_dirs=("$HOME/miniconda3/envs" "$HOME/anaconda3/envs" "$HOME/miniforge3/envs" "$HOME/mambaforge/envs")
for conda_root in "${conda_dirs[@]}"; do
if [[ -d "$conda_root" ]]; then
while IFS= read -r dist_info; do
[[ -z "$dist_info" ]] && continue
ver=$(extract_version_from_path "$dist_info")
env_name=$(echo "$dist_info" | sed "s|$conda_root/||" | cut -d'/' -f1)
grep -Fqx "$conda_root/$env_name|$ver" "$SEEN_INSTALLS_FILE" 2>/dev/null && continue
printf '%s|%s\n' "$conda_root/$env_name" "$ver" >> "$SEEN_INSTALLS_FILE"
venv_found=$((venv_found + 1))
if is_bad_version "$ver"; then
unsafe "Compromised litellm $ver in conda env '$env_name'"
else
safe "litellm $ver in conda env '$env_name'"
fi
inspect_installed_proxy_payload "$dist_info" "$ver" || true
done < <(find "$conda_root" -maxdepth 6 -path "*/site-packages/litellm-*.dist-info" -type d 2>/dev/null || true)
fi
done
# --- global pip install ---
global_ver=$(python3 -m pip show litellm 2>/dev/null | grep "^Version:" | sed 's/Version: //' || true)
if [[ -n "$global_ver" ]]; then
venv_found=$((venv_found + 1))
if is_bad_version "$global_ver"; then
unsafe "Compromised litellm $global_ver installed globally (pip)"
else
safe "litellm $global_ver installed globally (pip)"
fi
fi
if [[ $venv_found -eq 0 ]]; then
info "No litellm installations found"
fi
# ── 4. Lock file audit ──────────────────────────────────────────────
header "4. Lock file audit"
lock_found=0
# Scan from workstation roots, not just cwd
for lock_root in "${SCAN_ROOTS[@]}"; do
[[ ! -d "$lock_root" ]] && continue
while IFS= read -r lockfile; do
[[ -z "$lockfile" ]] && continue
# Resolve to absolute and dedup
abs_lock=$(cd "$(dirname "$lockfile")" && pwd)/$(basename "$lockfile")
grep -Fqx "$abs_lock" "$SEEN_LOCKS_FILE" 2>/dev/null && continue
printf '%s\n' "$abs_lock" >> "$SEEN_LOCKS_FILE"
lock_found=1
versions=$(parse_lockfile_versions "$lockfile")
if [[ -n "$versions" ]]; then
while IFS= read -r ver; do
[[ -z "$ver" ]] && continue
if is_bad_version "$ver"; then
unsafe "$(basename "$lockfile") pins litellm $ver$abs_lock"
else
safe "$(basename "$lockfile") pins litellm $ver$abs_lock"
fi
done <<< "$versions"
fi
done < <(find "$lock_root" -maxdepth "$LOCKFILE_MAX_DEPTH" \
"${BROAD_PRUNE_ARGS[@]}" \
\( -name "uv.lock" -o -name "poetry.lock" -o -name "Pipfile.lock" -o -name "requirements*.txt" \) \
-type f -print 2>/dev/null || true)
done
if [[ $lock_found -eq 0 ]]; then
info "No Python lock files found"
fi
# ── 5. Docker containers ────────────────────────────────────────────
header "5. Docker containers"
if command -v docker &>/dev/null && docker info &>/dev/null 2>&1; then
containers=$(docker ps -aq 2>/dev/null || true)
if [[ -n "$containers" ]]; then
docker_hit=0
stopped_count=0
stopped_examples=""
while IFS= read -r cid; do
[[ -z "$cid" ]] && continue
cname=$(docker inspect --format '{{.Name}}' "$cid" 2>/dev/null | sed 's|^/||')
cstatus=$(docker inspect --format '{{.State.Status}}' "$cid" 2>/dev/null || true)
cimage=$(docker inspect --format '{{.Config.Image}}' "$cid" 2>/dev/null || true)
if [[ "$cstatus" != "running" ]]; then
stopped_count=$((stopped_count + 1))
if [[ $stopped_count -le 5 ]]; then
stopped_examples+=$'\n'" $cname ($cstatus) — $cimage"
fi
continue
fi
# Check for .pth payload inside container
pth_in_container=$(docker exec "$cid" find / -name "litellm_init.pth" -maxdepth 8 2>/dev/null | head -3 || true)
if [[ -n "$pth_in_container" ]]; then
unsafe ".pth payload in container '$cname': $pth_in_container"
docker_hit=1
fi
# Check installed litellm version
container_ver=$(docker exec "$cid" python3 -m pip show litellm 2>/dev/null | grep "^Version:" | sed 's/Version: //' || true)
if [[ -n "$container_ver" ]]; then
if is_bad_version "$container_ver"; then
unsafe "Container '$cname' has litellm $container_ver"
docker_hit=1
else
safe "Container '$cname' has litellm $container_ver"
fi
fi
done <<< "$containers"
if [[ $stopped_count -gt 0 ]]; then
info "$stopped_count non-running container(s) found; not inspected live"
[[ -n "$stopped_examples" && $JSON_OUTPUT -eq 0 ]] && printf "%s\n" "$stopped_examples"
fi
[[ $docker_hit -eq 0 ]] && safe "No compromised litellm in running containers"
else
info "No running containers"
fi
suspicious_images=$(docker images --format '{{.Repository}}:{{.Tag}}' 2>/dev/null | grep -E '(^|/)litellm|smolagents' || true)
if [[ -n "$suspicious_images" ]]; then
warning "Docker images referencing litellm/smolagents found; audit before use:"
echo "$suspicious_images" | while IFS= read -r img; do printf " %s\n" "$img"; done
while IFS= read -r img; do
[[ -z "$img" ]] && continue
archive_iocs=$(run_shell_with_timeout 30 "docker image save \"$img\" | tar -tf - | grep -E 'litellm_init\\.pth|sysmon\\.py|tpcp\\.tar\\.gz'")
if [[ -n "$archive_iocs" ]]; then
unsafe "Suspicious files present in offline Docker image '$img':"
echo "$archive_iocs" | head -10 | while IFS= read -r hit; do printf " %s\n" "$hit"; done
fi
history_iocs=$(run_with_timeout 15 docker history --no-trunc "$img" | grep -Ei 'models\.litellm\.cloud|checkmarx\.zone|tpcp\.tar\.gz|metadata\.google\.internal|169\.254\.169\.254|169\.254\.170\.2' || true)
if [[ -n "$history_iocs" ]]; then
warning "Suspicious strings found in Docker image history for '$img':"
echo "$history_iocs" | head -10 | while IFS= read -r hit; do printf " %s\n" "$hit"; done
fi
done <<< "$(echo "$suspicious_images" | head -"$MAX_IMAGE_AUDIT")"
fi
else
skip "Docker not available"
fi
# ── 6. Backdoor persistence checks ─────────────────────────────────
header "6. Backdoor persistence checks"
# sysmon backdoor
if [[ -f "$HOME/.config/sysmon/sysmon.py" ]]; then
unsafe "Backdoor found: ~/.config/sysmon/sysmon.py"
info "Contents (first 5 lines):"
head -5 "$HOME/.config/sysmon/sysmon.py" | while IFS= read -r line; do printf " %s\n" "$line"; done
else
safe "No ~/.config/sysmon/sysmon.py backdoor"
fi
# systemd persistence (unlikely on macOS but check anyway)
if [[ -f "$HOME/.config/systemd/user/sysmon.service" ]]; then
unsafe "Backdoor systemd service: ~/.config/systemd/user/sysmon.service"
report_sha256 "$HOME/.config/systemd/user/sysmon.service"
else
safe "No sysmon.service persistence"
fi
if command -v systemctl &>/dev/null; then
sysmon_active="$(run_with_timeout 5 systemctl --user is-active sysmon.service | tr -d '[:space:]')"
sysmon_enabled="$(run_with_timeout 5 systemctl --user is-enabled sysmon.service | tr -d '[:space:]')"
if [[ "$sysmon_active" == "active" || "$sysmon_enabled" == "enabled" ]]; then
unsafe "sysmon.service is active or enabled under the current user"
sysmon_status="$(run_with_timeout 5 systemctl --user status sysmon.service || true)"
[[ -n "$sysmon_status" ]] && echo "$sysmon_status" | head -20 | while IFS= read -r line; do printf " %s\n" "$line"; done
else
info "sysmon.service is not active under the current user"
fi
else
info "systemctl not installed; skipping sysmon.service runtime status check"
fi
if [[ -f "/root/.config/sysmon/sysmon.py" ]]; then
unsafe "Backdoor found: /root/.config/sysmon/sysmon.py"
else
info "No /root/.config/sysmon/sysmon.py backdoor visible"
fi
# launchd persistence (macOS equivalent)
malicious_plist=$(find "$HOME/Library/LaunchAgents" -name "*sysmon*" -o -name "*litellm*" 2>/dev/null || true)
if [[ -n "$malicious_plist" ]]; then
unsafe "Suspicious LaunchAgent found:"
echo "$malicious_plist" | while IFS= read -r f; do printf " %s\n" "$f"; done
else
safe "No suspicious LaunchAgents"
fi
system_plist=$(find /Library/LaunchAgents /Library/LaunchDaemons \( -name "*sysmon*" -o -name "*litellm*" \) 2>/dev/null || true)
if [[ -n "$system_plist" ]]; then
unsafe "Suspicious system launch agent/daemon found:"
echo "$system_plist" | while IFS= read -r f; do printf " %s\n" "$f"; done
else
info "No suspicious system launch agents/daemons visible"
fi
cron_hit=$(crontab -l 2>/dev/null | grep -iE "sysmon|litellm|tpcp" || true)
if [[ -n "$cron_hit" ]]; then
unsafe "Suspicious crontab entries found:"
echo "$cron_hit" | while IFS= read -r line; do printf " %s\n" "$line"; done
else
safe "No suspicious crontab entries"
fi
checkmarx_hits=$(find "$HOME/.config" "$HOME/.kube" "$HOME/.ssh" "$HOME/.aws" "$HOME/.azure" "$HOME/.config/gcloud" \
-type f -exec grep -HnE 'checkmarx\.zone/raw|checkmarx\.zone' {} + 2>/dev/null | head -20 || true)
if [[ -n "$checkmarx_hits" ]]; then
unsafe "checkmarx.zone IOC strings found in sensitive config paths:"
echo "$checkmarx_hits" | while IFS= read -r line; do printf " %s\n" "$line"; done
else
safe "No checkmarx.zone IOC strings found in sensitive config paths"
fi
# ── 7. Kubernetes indicators ────────────────────────────────────────
header "7. Kubernetes lateral movement"
if command -v kubectl &>/dev/null; then
if [[ "$(run_with_timeout 5 kubectl auth can-i list pods -n kube-system | tr -d '[:space:]')" == "yes" ]]; then
bad_pods=$(run_with_timeout 10 kubectl get pods -n kube-system -o name | grep "node-setup" || true)
if [[ -n "$bad_pods" ]]; then
unsafe "Suspicious node-setup pods in kube-system:"
echo "$bad_pods" | while IFS= read -r p; do printf " %s\n" "$p"; done
else
safe "No node-setup-* pods in kube-system"
fi
privileged_pods=$(run_with_timeout 15 kubectl get pods -A -o jsonpath='{range .items[*]}{.metadata.namespace}{"|"}{.metadata.name}{"|"}{range .spec.containers[*]}{.securityContext.privileged}{" "}{end}{"|"}{range .spec.volumes[*]}{.hostPath.path}{" "}{end}{"\n"}{end}' \
| grep -E '(^|.*\|)true|/($|root|etc|var|usr)' || true)
if [[ -n "$privileged_pods" ]]; then
warning "Privileged or host-mounted Kubernetes pods found; review for malicious alpine/node setup patterns:"
echo "$privileged_pods" | head -10 | while IFS= read -r p; do printf " %s\n" "$p"; done
else
safe "No obvious privileged/host-mounted pods found"
fi
else
skip "Cannot list kube-system pods (no access, no cluster, or timed out)"
fi
if [[ "$(run_with_timeout 5 kubectl auth can-i list secrets --all-namespaces | tr -d '[:space:]')" == "yes" ]]; then
warning "Risk note: current Kubernetes credentials can list secrets across namespaces; treat cluster secrets as exposed if host compromise is confirmed"
else
info "Cannot list Kubernetes secrets across namespaces"
fi
else
skip "kubectl not installed"
fi
if [[ -f "$HOME/.kube/config" ]]; then
warning "Risk note: ~/.kube/config exists on this workstation; if compromise is confirmed, treat cluster access as exposed"
else
safe "No ~/.kube/config present"
fi
# ── 8. Network indicators ──────────────────────────────────────────
header "8. Network indicators"
# Check DNS logs before doing any resolver lookup ourselves.
exfil_domain="models.litellm.cloud"
dns_hit=$(run_shell_with_timeout 10 "log show --predicate 'process == \"mDNSResponder\" && eventMessage contains \"$exfil_domain\"' --last 24h --style compact | head -5")
if [[ -n "$dns_hit" ]]; then
warning "DNS queries to $exfil_domain found in last 24h system logs"
echo "$dns_hit" | while IFS= read -r d; do printf " %s\n" "$d"; done
else
safe "No DNS queries to $exfil_domain in last 24h"
fi
# Resolve exfil domain afterward to obtain an IP for active connection checks.
exfil_ip=""
if host_output=$(host "$exfil_domain" 2>/dev/null); then
exfil_ip=$(echo "$host_output" | grep "has address" | head -1 | awk '{print $NF}' || true)
info "Exfil domain $exfil_domain resolves (IP: ${exfil_ip:-unknown}) — used only to widen the active connection check"
else
info "Exfil domain $exfil_domain does not resolve (may be taken down)"
fi
# Check for active connections — match domain name AND resolved IP
conn_pattern="litellm"
[[ -n "$exfil_ip" ]] && conn_pattern="litellm|$exfil_ip"
active_conn=$(lsof -i -n -P 2>/dev/null | grep -iE "$conn_pattern" || true)
if [[ -n "$active_conn" ]]; then
unsafe "Active network connection to litellm infrastructure detected:"
echo "$active_conn" | while IFS= read -r c; do printf " %s\n" "$c"; done
else
safe "No active connections to litellm infrastructure"
fi
# ── 9. Suspicious process scan ─────────────────────────────────────
header "9. Suspicious process scan"
sus_procs=$(ps aux 2>/dev/null | grep -E "(litellm_init|sysmon\.py|models\.litellm)" | grep -v grep || true)
if [[ -n "$sus_procs" ]]; then
unsafe "Suspicious processes running:"
echo "$sus_procs" | while IFS= read -r p; do printf " %s\n" "$p"; done
else
safe "No suspicious processes detected"
fi
fork_bomb_procs=$(ps aux 2>/dev/null | awk '/python/ && !/awk/ {count++} END {if (count > 50) print count}' || true)
if [[ -n "$fork_bomb_procs" ]]; then
warning "High Python process count detected ($fork_bomb_procs) — investigate possible .pth-triggered process fan-out"
else
safe "No obvious runaway Python process fan-out"
fi
# ── 10. Sensitive file access times ─────────────────────────────────
header "10. Sensitive file access analysis"
# The malware harvests .env files, shell history, SSH keys, and credentials.
# Check if any were accessed in the last 8 hours (attack window heuristic).
info "Checking access times on sensitive files (last 8 hours)..."
info "macOS APFS access times may be disabled or unreliable, so recent-access checks are heuristic only"
# .env files in dev directories
env_recently_accessed=$(find "${SCAN_ROOTS[@]}" \
"${BROAD_PRUNE_ARGS[@]}" \
-name ".env" -amin -480 -type f -print 2>/dev/null | head -20 || true)
if [[ -n "$env_recently_accessed" ]]; then
count=$(echo "$env_recently_accessed" | wc -l | tr -d ' ')
warning "$count .env file(s) accessed in the last 8 hours (normal if you were working):"
echo "$env_recently_accessed" | head -5 | while IFS= read -r f; do printf " %s\n" "$f"; done
[[ $count -gt 5 ]] && info " ... and $((count - 5)) more"
else
safe "No .env files accessed in last 8 hours"
fi
# Shell history access
history_files=("$HOME/.bash_history" "$HOME/.zsh_history" "$HOME/.local/share/fish/fish_history")
hist_accessed=0
for hf in "${history_files[@]}"; do
if [[ -f "$hf" ]]; then
# Check if accessed in last 8 hours (atime)
if find "$hf" -amin -480 2>/dev/null | grep -q .; then
warning "Shell history accessed recently (common on active workstations): $hf"
hist_accessed=1
fi
fi
done
[[ $hist_accessed -eq 0 ]] && safe "No shell history files accessed in last 8 hours"
# SSH keys
ssh_accessed=$(find "$HOME/.ssh" -name "id_*" ! -name "*.pub" -amin -480 2>/dev/null || true)
if [[ -n "$ssh_accessed" ]]; then
warning "SSH private key(s) accessed in last 8 hours:"
echo "$ssh_accessed" | while IFS= read -r f; do printf " %s\n" "$f"; done
else
safe "No SSH private keys accessed in last 8 hours"
fi
metadata_hits=$(grep -hE '169\.254\.169\.254|metadata\.google\.internal|169\.254\.170\.2' "$HOME/.bash_history" "$HOME/.zsh_history" "$HOME/.local/share/fish/fish_history" 2>/dev/null || true)
if [[ -n "$metadata_hits" ]]; then
warning "Cloud metadata endpoint references found in shell history:"
echo "$metadata_hits" | head -5 | while IFS= read -r line; do printf " %s\n" "$line"; done
else
safe "No cloud metadata endpoint references found in shell history"
fi
# ── 11. Artifact sweep ──────────────────────────────────────────────
header "11. Residual artifact sweep"
artifact_roots=("${SCAN_ROOTS[@]}" "$HOME/.config" "$HOME/.kube" "$HOME/.ssh" "$HOME/.aws" "$HOME/.azure" "$HOME/.config/gcloud")
temp_artifact_hits=""
for temp_root in "/tmp" "${TMPDIR:-/tmp}"; do
[[ ! -d "$temp_root" ]] && continue
while IFS= read -r temp_hit; do
[[ -z "$temp_hit" ]] && continue
temp_artifact_hits="${temp_artifact_hits}${temp_hit}"$'\n'
done < <(find "$temp_root" -maxdepth 2 \
\( -name "tpcp.tar.gz" -o -name "session.key" -o -name "payload.enc" -o -name "session.key.enc" -o -name "pglog" -o -name ".pg_state" \) \
-type f -print 2>/dev/null || true)
done
temp_artifact_hits="$(printf '%s' "$temp_artifact_hits" | dedupe_paths || true)"
if [[ -n "$temp_artifact_hits" ]]; then
unsafe "Suspicious temporary exfiltration artifacts found:"
echo "$temp_artifact_hits" | while IFS= read -r f; do
[[ -z "$f" ]] && continue
printf " %s\n" "$f"
report_sha256 "$f"
done
else
safe "No suspicious temporary exfiltration artifacts found"
fi
artifact_hits=$(find "${artifact_roots[@]}" \
"${BROAD_PRUNE_ARGS[@]}" \
\( -name "tpcp.tar.gz" -o -name "litellm_init.pth" -o -path "*/.config/sysmon/sysmon.py" -o -path "*/.config/systemd/user/sysmon.service" \) \
-type f -print 2>/dev/null | head -20 || true)
if [[ -n "$artifact_hits" ]]; then
unsafe "Suspicious residual artifact files found:"
echo "$artifact_hits" | while IFS= read -r f; do
[[ -z "$f" ]] && continue
printf " %s\n" "$f"
report_sha256 "$f"
done
else
safe "No obvious residual artifact files found"
fi
ioc_text_hits=$(find "${artifact_roots[@]}" \
"${BROAD_PRUNE_ARGS[@]}" \
-path "$SELF_PATH" -prune -o \
\( -name "*.log" -o -name "*.txt" -o -name "*.out" -o -name "*.err" -o -name "*.json" -o -name "*.yaml" -o -name "*.yml" -o -name "*.env" -o -name "*.sh" -o -name "*.py" -o -name "*.toml" -o -name "*.md" \) \
-type f -exec grep -HnE 'models\.litellm\.cloud|checkmarx\.zone|tpcp\.tar\.gz|docs-tpcp|tpcp-docs' {} + 2>/dev/null | head -20 || true)
if [[ -n "$ioc_text_hits" ]]; then
warning "Suspicious IOC strings found in local text files (documentation hits may be benign and require human review):"
echo "$ioc_text_hits" | while IFS= read -r line; do printf " %s\n" "$line"; done
else
safe "No suspicious IOC strings found in local text files"
fi
# ── 12. Credential exposure assessment ──────────────────────────────
header "12. Credential exposure risk"
cred_files=(
"$HOME/.ssh/id_rsa"
"$HOME/.ssh/id_ed25519"
"$HOME/.aws/credentials"
"$HOME/.config/gcloud/application_default_credentials.json"
"$HOME/.azure/accessTokens.json"
"$HOME/.kube/config"
"$HOME/.netrc"
"$HOME/.npmrc"
"$HOME/.pypirc"
)
exposed_count=0
for cf in "${cred_files[@]}"; do
[[ -f "$cf" ]] && exposed_count=$((exposed_count + 1))
done
if [[ $UNSAFE -gt 0 && $exposed_count -gt 0 ]]; then
unsafe "$exposed_count credential files present — ROTATE IMMEDIATELY if compromise confirmed"
for cf in "${cred_files[@]}"; do
[[ -f "$cf" ]] && info " exists: $cf"
done
elif [[ $exposed_count -gt 0 ]]; then
info "$exposed_count credential files present (not at risk unless compromise is confirmed)"
else
safe "No standard credential files found"
fi
# ── Summary ─────────────────────────────────────────────────────────
printf "\n${BOLD}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}\n"
audit_timestamp="$(date -u '+%Y-%m-%d %H:%M:%S UTC')"
if [[ $JSON_OUTPUT -eq 1 ]]; then
python3 - "$FINDINGS_FILE" "$UNSAFE" "$WARN" "$MODE" "$audit_timestamp" "$BAD_VERSIONS" <<'PY' >&3
import json
import sys
findings_path, unsafe_count, warn_count, mode, audited_at, bad_versions = sys.argv[1:7]
findings = []
with open(findings_path, "r", encoding="utf-8") as handle:
for raw_line in handle:
raw_line = raw_line.rstrip("\n")
if not raw_line:
continue
section, severity, status, message = raw_line.split("\t", 3)
findings.append(
{
"section": section,
"severity": severity,
"status": status,
"message": message,
}
)
unsafe_count = int(unsafe_count)
warn_count = int(warn_count)
summary = {
"status": "compromised" if unsafe_count else ("warning" if warn_count else "clear"),
"unsafe_count": unsafe_count,
"warning_count": warn_count,
"mode": mode,
"audited_at": audited_at,
"bad_versions": bad_versions.split("|"),
"findings": findings,
}
print(json.dumps(summary, indent=2))
PY
else
if [[ $UNSAFE -gt 0 ]]; then
printf "${RED}${BOLD} COMPROMISED — %d critical finding(s)${RESET}\n" "$UNSAFE"
printf "\n ${BOLD}Immediate actions:${RESET}\n"
printf " 1. Disconnect from network\n"
printf " 2. Remove litellm 1.82.7/1.82.8 from all envs\n"
printf " 3. Purge caches: rm -rf ~/.cache/uv ~/.cache/pip\n"
printf " 4. Delete backdoor: rm -rf ~/.config/sysmon\n"
printf " 5. Rotate ALL credentials (SSH, AWS, GCP, Azure, k8s, .netrc, .pypirc)\n"
printf " 6. If k8s access existed: audit cluster for node-setup-* pods\n"
printf " 7. Check Docker containers: docker ps -a\n"
printf " 8. Notify security team\n"
elif [[ $WARN -gt 0 ]]; then
printf "${YELLOW}${BOLD} LIKELY SAFE — but %d warning(s) to review${RESET}\n" "$WARN"
else
printf "${GREEN}${BOLD} ALL CLEAR — no indicators of compromise found${RESET}\n"
fi
printf "${BOLD}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}\n"
printf "${DIM} Audited: %s${RESET}\n" "$audit_timestamp"
printf "${DIM} Mode: %s${RESET}\n" "$MODE"
printf "${DIM} Threat: litellm 1.82.7/1.82.8 supply-chain attack${RESET}\n"
printf "${DIM} Ref: https://github.com/BerriAI/litellm/issues/24512${RESET}\n\n"
fi
exit $(( UNSAFE > 0 ? 1 : 0 ))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment