Last active
March 26, 2026 00:56
-
-
Save pwittchen/8d594ec0f570e36788bcd61fefbb8ea9 to your computer and use it in GitHub Desktop.
bash script for checking litellm vulnerability in the current OS (macOS/Linux)
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.sh — Detect current or past installation of compromised | |
| # litellm versions (1.82.7, 1.82.8) from the March 2026 supply chain attack. | |
| # | |
| # Works on macOS and Linux. Checks pip, pip3, poetry, conda, and common | |
| # virtual-environment locations. | |
| # | |
| # Usage: bash check_litellm.sh | |
| # | |
| # Reference: https://docs.litellm.ai/blog/security-update-march-2026 | |
| set -euo pipefail | |
| RED='\033[0;31m' | |
| GREEN='\033[0;32m' | |
| YELLOW='\033[0;33m' | |
| BOLD='\033[1m' | |
| RESET='\033[0m' | |
| FOUND_ISSUES=0 | |
| WARN_COUNT=0 | |
| OK_COUNT=0 | |
| INFO_COUNT=0 | |
| WARNINGS=() | |
| WARN_DETAILS=() | |
| OK_MSGS=() | |
| INFO_MSGS=() | |
| OS="$(uname -s)" | |
| # Tracking variables for summary detail sections | |
| UNIFIED_LOG_STATUS="" # "clean", "found", or "skipped" | |
| UNIFIED_LOG_HITS="" | |
| VARLOG_STATUS="" # "clean", "found", or "" | |
| VARLOG_HITS="" | |
| JOURNAL_STATUS="" # "clean", "found", or "" | |
| JOURNAL_HITS="" | |
| PIP_LOG_STATUS="" # "clean", "found_malicious", "found_nonmalicious", or "clean" | |
| PIP_LOG_DETAILS="" | |
| TOTAL_STEPS=9 | |
| CURRENT_STEP=0 | |
| TERM_COLS="${COLUMNS:-$(tput cols 2>/dev/null || echo 80)}" | |
| LOG_FILE="/tmp/check_litellm_$(date +%Y%m%d_%H%M%S).log" | |
| progress() { | |
| local msg=" $1" | |
| # Truncate to terminal width to avoid wrapping | |
| if (( ${#msg} > TERM_COLS - 1 )); then | |
| msg="${msg:0:$((TERM_COLS - 4))}..." | |
| fi | |
| # Write directly to terminal so progress lines stay out of the log file | |
| printf '\r%-*s\r' "$TERM_COLS" "" >/dev/tty 2>/dev/null || true | |
| printf '\r%s' "$msg" >/dev/tty 2>/dev/null || true | |
| } | |
| clear_progress() { | |
| printf '\r%-*s\r' "$TERM_COLS" "" >/dev/tty 2>/dev/null || true | |
| } | |
| warn() { FOUND_ISSUES=1; WARN_COUNT=$((WARN_COUNT + 1)); WARNINGS+=("$*"); WARN_DETAILS+=(""); } | |
| # Append detail lines to the most recent warning (for the summary) | |
| warn_detail() { | |
| local idx=$(( ${#WARN_DETAILS[@]} - 1 )) | |
| if [[ -n "${WARN_DETAILS[$idx]}" ]]; then | |
| WARN_DETAILS[$idx]="${WARN_DETAILS[$idx]}"$'\n'"$1" | |
| else | |
| WARN_DETAILS[$idx]="$1" | |
| fi | |
| } | |
| ok() { OK_COUNT=$((OK_COUNT + 1)); OK_MSGS+=("$*"); } | |
| info() { INFO_COUNT=$((INFO_COUNT + 1)); INFO_MSGS+=("$*"); } | |
| header(){ CURRENT_STEP=$((CURRENT_STEP + 1)); progress "[${CURRENT_STEP}/${TOTAL_STEPS}] $*"; } | |
| MALICIOUS_VERSIONS="1.82.7|1.82.8" | |
| C2_DOMAIN="models.litellm.cloud" | |
| PTH_FILE="litellm_init.pth" | |
| # --------------------------------------------------------------------------- | |
| # Helpers | |
| # --------------------------------------------------------------------------- | |
| check_pip_variant() { | |
| local pip_cmd="$1" | |
| if ! command -v "$pip_cmd" &>/dev/null; then | |
| info "$pip_cmd not found, skipping." | |
| return | |
| fi | |
| local pip_show | |
| pip_show=$("$pip_cmd" show litellm 2>/dev/null) || true | |
| local version | |
| version=$(echo "$pip_show" | grep -i '^Version:' | awk '{print $2}') || true | |
| local location | |
| location=$(echo "$pip_show" | grep -i '^Location:' | awk '{print $2}') || true | |
| if [[ -z "$version" ]]; then | |
| ok "litellm is not installed via $pip_cmd." | |
| elif echo "$version" | grep -qE "^($MALICIOUS_VERSIONS)$"; then | |
| warn "MALICIOUS litellm $version is installed via $pip_cmd!${location:+ (${location})}" | |
| else | |
| info "litellm $version is installed via $pip_cmd (not a known malicious version)." | |
| fi | |
| local cache | |
| cache=$("$pip_cmd" cache list litellm 2>/dev/null) || true | |
| if [[ -n "$cache" ]]; then | |
| if echo "$cache" | grep -qE "litellm.*(${MALICIOUS_VERSIONS})"; then | |
| local bad_cache | |
| bad_cache=$(echo "$cache" | grep -E "(${MALICIOUS_VERSIONS})") | |
| warn "Malicious litellm version found in $pip_cmd cache!" | |
| warn_detail "$bad_cache" | |
| else | |
| info "litellm found in $pip_cmd cache (non-malicious version)." | |
| fi | |
| else | |
| ok "No litellm in $pip_cmd cache." | |
| fi | |
| } | |
| check_poetry() { | |
| if ! command -v poetry &>/dev/null; then | |
| info "poetry not found, skipping." | |
| return | |
| fi | |
| # Check the global poetry cache | |
| local cache_dir | |
| cache_dir=$(poetry config cache-dir 2>/dev/null) || true | |
| if [[ -n "$cache_dir" && -d "$cache_dir" ]]; then | |
| local hits | |
| hits=$(find "$cache_dir" -type f -name "*.whl" -o -name "*.tar.gz" 2>/dev/null \ | |
| | grep -i "litellm" || true) | |
| if [[ -n "$hits" ]]; then | |
| if echo "$hits" | grep -qE "(${MALICIOUS_VERSIONS})"; then | |
| local bad_hits | |
| bad_hits=$(echo "$hits" | grep -E "(${MALICIOUS_VERSIONS})") | |
| warn "Malicious litellm artifact found in poetry cache!" | |
| warn_detail "$bad_hits" | |
| else | |
| info "litellm found in poetry cache (non-malicious version)." | |
| fi | |
| else | |
| ok "No litellm in poetry cache." | |
| fi | |
| fi | |
| # Scan poetry.lock files under $HOME for litellm | |
| info "Scanning for poetry.lock files referencing litellm under \$HOME (this may take a moment)..." | |
| local locks | |
| locks=$(find "$HOME" -maxdepth 6 -name "poetry.lock" -type f 2>/dev/null || true) | |
| if [[ -n "$locks" ]]; then | |
| local lock_arr=() | |
| while IFS= read -r l; do lock_arr+=("$l"); done <<< "$locks" | |
| local lock_total=${#lock_arr[@]} | |
| local lock_i=0 | |
| for lockfile in "${lock_arr[@]}"; do | |
| lock_i=$((lock_i + 1)) | |
| progress "Scanning poetry.lock ($lock_i/$lock_total)... $lockfile" | |
| if grep -qi "litellm" "$lockfile" 2>/dev/null; then | |
| local lock_ver | |
| lock_ver=$(grep -A2 'name = "litellm"' "$lockfile" 2>/dev/null \ | |
| | grep 'version' | head -1 | sed 's/.*"\(.*\)".*/\1/' || true) | |
| if echo "$lock_ver" | grep -qE "^($MALICIOUS_VERSIONS)$"; then | |
| warn "poetry.lock pins MALICIOUS litellm $lock_ver: $lockfile" | |
| else | |
| info "poetry.lock references litellm $lock_ver: $lockfile" | |
| fi | |
| fi | |
| done | |
| clear_progress | |
| else | |
| ok "No poetry.lock files found." | |
| fi | |
| } | |
| check_conda() { | |
| if ! command -v conda &>/dev/null; then | |
| info "conda not found, skipping." | |
| return | |
| fi | |
| local envs | |
| envs=$(conda env list 2>/dev/null | grep -v '^#' | awk '{print $1}' | grep -v '^$' || true) | |
| if [[ -z "$envs" ]]; then | |
| ok "No conda environments found." | |
| return | |
| fi | |
| local env_arr=() | |
| while IFS= read -r e; do env_arr+=("$e"); done <<< "$envs" | |
| local env_total=${#env_arr[@]} | |
| local env_i=0 | |
| for env in "${env_arr[@]}"; do | |
| env_i=$((env_i + 1)) | |
| progress "Scanning conda env ($env_i/$env_total)... $env" | |
| local hit | |
| hit=$(conda list -n "$env" 2>/dev/null | grep -i litellm || true) | |
| if [[ -n "$hit" ]]; then | |
| if echo "$hit" | grep -qE "(${MALICIOUS_VERSIONS})"; then | |
| warn "Malicious litellm in conda env '$env': $hit" | |
| else | |
| info "litellm in conda env '$env' (non-malicious): $hit" | |
| fi | |
| fi | |
| done | |
| clear_progress | |
| ok "Finished scanning conda environments." | |
| } | |
| check_venvs() { | |
| info "Searching for virtual environments under \$HOME (this may take a moment)..." | |
| local site_dirs | |
| site_dirs=$(find "$HOME" -maxdepth 6 \ | |
| \( -path "*/lib/python*/site-packages" -o -path "*/lib/site-packages" \) \ | |
| -type d 2>/dev/null || true) | |
| if [[ -z "$site_dirs" ]]; then | |
| ok "No Python site-packages directories found." | |
| return | |
| fi | |
| local sp_arr=() | |
| while IFS= read -r s; do sp_arr+=("$s"); done <<< "$site_dirs" | |
| local sp_total=${#sp_arr[@]} | |
| local sp_i=0 | |
| local found_any=0 | |
| for sp in "${sp_arr[@]}"; do | |
| sp_i=$((sp_i + 1)) | |
| progress "Scanning site-packages ($sp_i/$sp_total)... $sp" | |
| # Check for litellm dist-info | |
| local dist | |
| dist=$(find "$sp" -maxdepth 1 -type d -name "litellm-*" 2>/dev/null || true) | |
| if [[ -n "$dist" ]]; then | |
| if echo "$dist" | grep -qE "litellm-(${MALICIOUS_VERSIONS})"; then | |
| warn "Malicious litellm dist-info found: $dist" | |
| else | |
| info "litellm dist-info found (non-malicious): $dist" | |
| fi | |
| found_any=1 | |
| fi | |
| # Check for the malicious .pth file | |
| if [[ -f "$sp/$PTH_FILE" ]]; then | |
| warn "MALICIOUS $PTH_FILE found: $sp/$PTH_FILE" | |
| fi | |
| done | |
| clear_progress | |
| if [[ "$found_any" -eq 0 ]]; then | |
| ok "No litellm installations found in virtual environments." | |
| fi | |
| } | |
| check_pth_file() { | |
| info "Searching entire filesystem for $PTH_FILE (this may take a while)..." | |
| local hits | |
| if [[ "$OS" == "Darwin" ]]; then | |
| # Use Spotlight first for speed, fall back to find | |
| progress "Searching filesystem (mdfind)... $PTH_FILE" | |
| hits=$(mdfind -name "$PTH_FILE" 2>/dev/null || true) | |
| if [[ -z "$hits" ]]; then | |
| progress "Searching filesystem (find /)... $PTH_FILE" | |
| hits=$(find / -name "$PTH_FILE" 2>/dev/null || true) | |
| fi | |
| else | |
| progress "Searching filesystem (find /)... $PTH_FILE" | |
| hits=$(find / -name "$PTH_FILE" 2>/dev/null || true) | |
| fi | |
| clear_progress | |
| if [[ -n "$hits" ]]; then | |
| warn "MALICIOUS .pth file found on disk!" | |
| warn_detail "$hits" | |
| else | |
| ok "No $PTH_FILE found." | |
| fi | |
| } | |
| check_persistence() { | |
| if [[ "$OS" == "Linux" ]]; then | |
| # systemd user services | |
| local user_systemd="$HOME/.config/systemd/user" | |
| progress "Checking dir... $user_systemd" | |
| if [[ -d "$user_systemd" ]]; then | |
| local suspicious | |
| suspicious=$(ls -1 "$user_systemd" 2>/dev/null || true) | |
| if [[ -n "$suspicious" ]]; then | |
| info "systemd user services found in $user_systemd — review manually: $suspicious" | |
| fi | |
| else | |
| ok "No systemd user service directory." | |
| fi | |
| # Also check system-level | |
| for dir in /etc/systemd/system /usr/lib/systemd/system; do | |
| progress "Checking dir... $dir" | |
| if [[ -d "$dir" ]]; then | |
| local hits | |
| hits=$(ls -1 "$dir" 2>/dev/null | grep -i litellm || true) | |
| if [[ -n "$hits" ]]; then | |
| warn "Suspicious systemd service referencing litellm in $dir!" | |
| warn_detail "$hits" | |
| fi | |
| fi | |
| done | |
| clear_progress | |
| fi | |
| if [[ "$OS" == "Darwin" ]]; then | |
| # Check LaunchAgents / LaunchDaemons (macOS persistence equivalent) | |
| for dir in "$HOME/Library/LaunchAgents" /Library/LaunchAgents /Library/LaunchDaemons; do | |
| progress "Checking dir... $dir" | |
| if [[ -d "$dir" ]]; then | |
| local hits | |
| hits=$(grep -rl "litellm\|$C2_DOMAIN" "$dir" 2>/dev/null || true) | |
| if [[ -n "$hits" ]]; then | |
| warn "Suspicious LaunchAgent/Daemon referencing litellm or C2 domain!" | |
| warn_detail "$hits" | |
| fi | |
| fi | |
| done | |
| clear_progress | |
| ok "No litellm-related LaunchAgents/Daemons found." | |
| fi | |
| } | |
| check_network_logs() { | |
| info "Checking system logs for C2 domain ($C2_DOMAIN)..." | |
| if [[ "$OS" == "Linux" ]]; then | |
| progress "Scanning /var/log/..." | |
| local hits | |
| hits=$(grep -r "$C2_DOMAIN" /var/log/ 2>/dev/null || true) | |
| if [[ -n "$hits" ]]; then | |
| local trimmed | |
| trimmed=$(echo "$hits" | head -20) | |
| warn "C2 domain found in /var/log/!" | |
| warn_detail "$trimmed" | |
| VARLOG_STATUS="found" | |
| VARLOG_HITS="$trimmed" | |
| else | |
| ok "No references to C2 domain in /var/log/." | |
| VARLOG_STATUS="clean" | |
| fi | |
| # Check journalctl if available | |
| if command -v journalctl &>/dev/null; then | |
| progress "Querying journalctl..." | |
| local jhits | |
| jhits=$(journalctl --no-pager -q 2>/dev/null | grep "$C2_DOMAIN" | head -5 || true) | |
| if [[ -n "$jhits" ]]; then | |
| warn "C2 domain found in journalctl!" | |
| warn_detail "$jhits" | |
| JOURNAL_STATUS="found" | |
| JOURNAL_HITS="$jhits" | |
| else | |
| ok "No references to C2 domain in journalctl." | |
| JOURNAL_STATUS="clean" | |
| fi | |
| fi | |
| clear_progress | |
| fi | |
| if [[ "$OS" == "Darwin" ]]; then | |
| progress "Scanning /var/log/..." | |
| local hits | |
| hits=$(grep -r "$C2_DOMAIN" /var/log/ 2>/dev/null || true) | |
| if [[ -n "$hits" ]]; then | |
| local trimmed | |
| trimmed=$(echo "$hits" | head -20) | |
| warn "C2 domain found in /var/log/!" | |
| warn_detail "$trimmed" | |
| VARLOG_STATUS="found" | |
| VARLOG_HITS="$trimmed" | |
| else | |
| ok "No references to C2 domain in /var/log/." | |
| VARLOG_STATUS="clean" | |
| fi | |
| # Check unified log (last 48h) | |
| if command -v log &>/dev/null; then | |
| progress "Querying unified log (last 48h)..." | |
| info "Checking macOS unified log (last 48h) — this may be slow..." | |
| local uhits | |
| uhits=$(log show --last 48h --predicate "eventMessage CONTAINS '$C2_DOMAIN'" 2>/dev/null \ | |
| | head -10 || true) | |
| if [[ -n "$uhits" && ! "$uhits" =~ "No matches" ]]; then | |
| warn "C2 domain found in macOS unified log!" | |
| warn_detail "$uhits" | |
| UNIFIED_LOG_STATUS="found" | |
| UNIFIED_LOG_HITS="$uhits" | |
| else | |
| ok "No references to C2 domain in macOS unified log (last 48h)." | |
| UNIFIED_LOG_STATUS="clean" | |
| fi | |
| else | |
| UNIFIED_LOG_STATUS="skipped" | |
| fi | |
| clear_progress | |
| fi | |
| } | |
| check_pip_logs() { | |
| local log_dirs=() | |
| if [[ "$OS" == "Darwin" ]]; then | |
| log_dirs+=("$HOME/Library/Logs/pip") | |
| fi | |
| log_dirs+=("$HOME/.pip" "$HOME/.cache/pip/log") | |
| local log_total=${#log_dirs[@]} | |
| local log_i=0 | |
| local found=0 | |
| for dir in "${log_dirs[@]}"; do | |
| log_i=$((log_i + 1)) | |
| progress "Scanning pip logs ($log_i/$log_total)... $dir" | |
| if [[ -d "$dir" ]]; then | |
| local hits | |
| hits=$(grep -r "litellm" "$dir" 2>/dev/null || true) | |
| if [[ -n "$hits" ]]; then | |
| if echo "$hits" | grep -qE "(${MALICIOUS_VERSIONS})"; then | |
| local bad_lines | |
| bad_lines=$(echo "$hits" | grep -E "(${MALICIOUS_VERSIONS})" | head -5) | |
| warn "Malicious litellm version referenced in pip logs: $dir" | |
| warn_detail "$bad_lines" | |
| PIP_LOG_STATUS="found_malicious" | |
| PIP_LOG_DETAILS="${PIP_LOG_DETAILS:+${PIP_LOG_DETAILS} | |
| }$bad_lines" | |
| else | |
| local sample_lines | |
| sample_lines=$(echo "$hits" | head -5) | |
| info "litellm referenced in pip logs (non-malicious version): $dir" | |
| if [[ "$PIP_LOG_STATUS" != "found_malicious" ]]; then | |
| PIP_LOG_STATUS="found_nonmalicious" | |
| fi | |
| PIP_LOG_DETAILS="${PIP_LOG_DETAILS:+${PIP_LOG_DETAILS} | |
| }[non-malicious] $dir: | |
| $sample_lines" | |
| fi | |
| found=1 | |
| fi | |
| fi | |
| done | |
| clear_progress | |
| if [[ "$found" -eq 0 ]]; then | |
| ok "No litellm references in pip logs." | |
| PIP_LOG_STATUS="clean" | |
| fi | |
| } | |
| check_shell_history() { | |
| local history_files=( | |
| "$HOME/.bash_history" | |
| "$HOME/.zsh_history" | |
| "$HOME/.local/share/fish/fish_history" | |
| ) | |
| local hist_total=${#history_files[@]} | |
| local hist_i=0 | |
| local found=0 | |
| for hf in "${history_files[@]}"; do | |
| hist_i=$((hist_i + 1)) | |
| progress "Checking history file ($hist_i/$hist_total)... $hf" | |
| if [[ -f "$hf" ]]; then | |
| local hits | |
| hits=$(grep -i "litellm" "$hf" 2>/dev/null || true) | |
| if [[ -n "$hits" ]]; then | |
| if echo "$hits" | grep -qE "(${MALICIOUS_VERSIONS})"; then | |
| local bad_lines | |
| bad_lines=$(echo "$hits" | grep -E "(${MALICIOUS_VERSIONS})" | head -10) | |
| warn "Malicious litellm version referenced in $hf!" | |
| warn_detail "$bad_lines" | |
| else | |
| info "litellm referenced in $hf (no malicious version string found)." | |
| fi | |
| found=1 | |
| fi | |
| fi | |
| done | |
| clear_progress | |
| if [[ "$found" -eq 0 ]]; then | |
| ok "No litellm references in shell history files." | |
| fi | |
| } | |
| # --------------------------------------------------------------------------- | |
| # Main | |
| # --------------------------------------------------------------------------- | |
| echo -e "${BOLD}=============================================${RESET}" | |
| echo -e "${BOLD} litellm Supply Chain Compromise Checker${RESET}" | |
| echo -e "${BOLD} Malicious versions: 1.82.7, 1.82.8${RESET}" | |
| echo -e "${BOLD} OS detected: $OS${RESET}" | |
| echo -e "${BOLD}=============================================${RESET}" | |
| header "Checking pip / pip3" | |
| check_pip_variant pip | |
| check_pip_variant pip3 | |
| header "Checking poetry" | |
| check_poetry | |
| header "Checking conda" | |
| check_conda | |
| header "Checking virtual environments" | |
| check_venvs | |
| header "Searching for malicious .pth file" | |
| check_pth_file | |
| header "Checking persistence mechanisms" | |
| check_persistence | |
| header "Checking network / system logs" | |
| check_network_logs | |
| header "Checking pip install logs" | |
| check_pip_logs | |
| header "Checking shell history" | |
| check_shell_history | |
| # --------------------------------------------------------------------------- | |
| # Summary | |
| # --------------------------------------------------------------------------- | |
| clear_progress | |
| echo "" | |
| echo -e "${BOLD}=============================================${RESET}" | |
| echo -e "${BOLD} Summary${RESET}" | |
| echo -e "${BOLD}=============================================${RESET}" | |
| echo "" | |
| echo -e " Checks passed: ${GREEN}${OK_COUNT}${RESET}" | |
| echo -e " Warnings: ${RED}${WARN_COUNT}${RESET}" | |
| echo -e " Informational: ${YELLOW}${INFO_COUNT}${RESET}" | |
| # --- System logs — C2 domain --- | |
| echo "" | |
| echo -e "${BOLD} System Logs — C2 domain ($C2_DOMAIN):${RESET}" | |
| case "$VARLOG_STATUS" in | |
| found) | |
| echo -e " ${RED}[!] C2 domain detected in /var/log/${RESET}" | |
| while IFS= read -r line; do | |
| echo " $line" | |
| done <<< "$VARLOG_HITS" | |
| ;; | |
| clean) | |
| echo -e " ${GREEN}[OK]${RESET} No references to C2 domain in /var/log/." | |
| ;; | |
| esac | |
| case "$JOURNAL_STATUS" in | |
| found) | |
| echo -e " ${RED}[!] C2 domain detected in journalctl${RESET}" | |
| while IFS= read -r line; do | |
| echo " $line" | |
| done <<< "$JOURNAL_HITS" | |
| ;; | |
| clean) | |
| echo -e " ${GREEN}[OK]${RESET} No references to C2 domain in journalctl." | |
| ;; | |
| esac | |
| case "$UNIFIED_LOG_STATUS" in | |
| found) | |
| echo -e " ${RED}[!] C2 domain detected in macOS unified log (last 48h)${RESET}" | |
| while IFS= read -r line; do | |
| echo " $line" | |
| done <<< "$UNIFIED_LOG_HITS" | |
| ;; | |
| clean) | |
| echo -e " ${GREEN}[OK]${RESET} No references to C2 domain in macOS unified log (last 48h)." | |
| ;; | |
| skipped) | |
| echo -e " ${YELLOW}[*]${RESET} macOS unified log check was skipped (log command not available)." | |
| ;; | |
| esac | |
| # --- pip install logs --- | |
| echo "" | |
| echo -e "${BOLD} pip Install Logs — litellm references:${RESET}" | |
| case "$PIP_LOG_STATUS" in | |
| found_malicious) | |
| echo -e " ${RED}[!] Malicious litellm version found in pip install logs${RESET}" | |
| while IFS= read -r line; do | |
| echo " $line" | |
| done <<< "$PIP_LOG_DETAILS" | |
| ;; | |
| found_nonmalicious) | |
| echo -e " ${YELLOW}[*]${RESET} litellm referenced in pip logs (non-malicious version)." | |
| while IFS= read -r line; do | |
| echo " $line" | |
| done <<< "$PIP_LOG_DETAILS" | |
| ;; | |
| clean) | |
| echo -e " ${GREEN}[OK]${RESET} No litellm references in pip install logs." | |
| ;; | |
| "") | |
| echo -e " ${YELLOW}[*]${RESET} pip log check did not run." | |
| ;; | |
| esac | |
| if [[ "$WARN_COUNT" -gt 0 ]]; then | |
| echo "" | |
| echo -e "${RED}${BOLD} Warnings:${RESET}" | |
| for i in "${!WARNINGS[@]}"; do | |
| echo -e " ${RED}[!]${RESET} ${WARNINGS[$i]}" | |
| if [[ -n "${WARN_DETAILS[$i]}" ]]; then | |
| while IFS= read -r detail_line; do | |
| echo " $detail_line" | |
| done <<< "${WARN_DETAILS[$i]}" | |
| fi | |
| done | |
| echo "" | |
| echo "Recommended actions:" | |
| echo " 1. Remove the malicious litellm package immediately." | |
| echo " 2. Rotate ALL credentials on this machine (SSH keys, API tokens," | |
| echo " cloud credentials, database passwords, .env files)." | |
| echo " 3. Treat the system as compromised — investigate for additional" | |
| echo " payloads and consider reimaging." | |
| echo "" | |
| echo "Reference: https://docs.litellm.ai/blog/security-update-march-2026" | |
| else | |
| echo "" | |
| echo -e "${GREEN}${BOLD} No signs of compromised litellm found.${RESET}" | |
| fi | |
| if [[ "${#INFO_MSGS[@]}" -gt 0 ]]; then | |
| echo "" | |
| echo -e "${YELLOW}${BOLD} Informational:${RESET}" | |
| for msg in "${INFO_MSGS[@]}"; do | |
| echo -e " ${YELLOW}[*]${RESET} $msg" | |
| done | |
| fi | |
| if [[ "${#OK_MSGS[@]}" -gt 0 ]]; then | |
| echo "" | |
| echo -e "${GREEN}${BOLD} Passed:${RESET}" | |
| for msg in "${OK_MSGS[@]}"; do | |
| echo -e " ${GREEN}[OK]${RESET} $msg" | |
| done | |
| fi | |
| echo "" | |
| echo -e "${BOLD}=============================================${RESET}" | |
| # Write plain-text log file | |
| { | |
| echo "=============================================" | |
| echo " litellm Supply Chain Compromise Checker" | |
| echo " Malicious versions: 1.82.7, 1.82.8" | |
| echo " OS detected: $OS" | |
| echo "=============================================" | |
| echo "" | |
| echo " Checks passed: $OK_COUNT" | |
| echo " Warnings: $WARN_COUNT" | |
| echo " Informational: $INFO_COUNT" | |
| echo "" | |
| echo " System Logs — C2 domain ($C2_DOMAIN):" | |
| case "$VARLOG_STATUS" in | |
| found) | |
| echo " [!] C2 domain detected in /var/log/" | |
| while IFS= read -r line; do | |
| echo " $line" | |
| done <<< "$VARLOG_HITS" | |
| ;; | |
| clean) | |
| echo " [OK] No references to C2 domain in /var/log/." | |
| ;; | |
| esac | |
| case "$JOURNAL_STATUS" in | |
| found) | |
| echo " [!] C2 domain detected in journalctl" | |
| while IFS= read -r line; do | |
| echo " $line" | |
| done <<< "$JOURNAL_HITS" | |
| ;; | |
| clean) | |
| echo " [OK] No references to C2 domain in journalctl." | |
| ;; | |
| esac | |
| case "$UNIFIED_LOG_STATUS" in | |
| found) | |
| echo " [!] C2 domain detected in macOS unified log (last 48h)" | |
| while IFS= read -r line; do | |
| echo " $line" | |
| done <<< "$UNIFIED_LOG_HITS" | |
| ;; | |
| clean) | |
| echo " [OK] No references to C2 domain in macOS unified log (last 48h)." | |
| ;; | |
| skipped) | |
| echo " [*] macOS unified log check was skipped (log command not available)." | |
| ;; | |
| esac | |
| echo "" | |
| echo " pip Install Logs — litellm references:" | |
| case "$PIP_LOG_STATUS" in | |
| found_malicious) | |
| echo " [!] Malicious litellm version found in pip install logs" | |
| while IFS= read -r line; do | |
| echo " $line" | |
| done <<< "$PIP_LOG_DETAILS" | |
| ;; | |
| found_nonmalicious) | |
| echo " [*] litellm referenced in pip logs (non-malicious version)." | |
| while IFS= read -r line; do | |
| echo " $line" | |
| done <<< "$PIP_LOG_DETAILS" | |
| ;; | |
| clean) | |
| echo " [OK] No litellm references in pip install logs." | |
| ;; | |
| "") | |
| echo " [*] pip log check did not run." | |
| ;; | |
| esac | |
| if [[ "$WARN_COUNT" -gt 0 ]]; then | |
| echo "" | |
| echo " Warnings:" | |
| for i in "${!WARNINGS[@]}"; do | |
| echo " [!] ${WARNINGS[$i]}" | |
| if [[ -n "${WARN_DETAILS[$i]}" ]]; then | |
| while IFS= read -r detail_line; do | |
| echo " $detail_line" | |
| done <<< "${WARN_DETAILS[$i]}" | |
| fi | |
| done | |
| fi | |
| if [[ "${#INFO_MSGS[@]}" -gt 0 ]]; then | |
| echo "" | |
| echo " Informational:" | |
| for msg in "${INFO_MSGS[@]}"; do | |
| echo " [*] $msg" | |
| done | |
| fi | |
| if [[ "${#OK_MSGS[@]}" -gt 0 ]]; then | |
| echo "" | |
| echo " Passed:" | |
| for msg in "${OK_MSGS[@]}"; do | |
| echo " [OK] $msg" | |
| done | |
| fi | |
| echo "" | |
| echo "=============================================" | |
| } > "$LOG_FILE" | |
| echo "" | |
| echo "Full log saved to: $LOG_FILE" | |
| exit $FOUND_ISSUES |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment