Last active
March 24, 2026 23:42
-
-
Save oneryalcin/c9e624278221503cab6045b1877348ca to your computer and use it in GitHub Desktop.
litellm supply-chain attack auditor for macOS workstations
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 | |
| # ────────────────────────────────────────────────────────────────────── | |
| # 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