URGENT: Python Supply Chain Attack Infecting AI Workflows
By CSharpner · March 28, 2026
There is a SERIOUS piece of malware infecting a wide range of Python software that you might be using right now. It has compromised the litellm package: a foundational library used by countless other Python packages and AI frameworks.
If you are a "vibe coder" or use AI agents, your tools may be automatically downloading, installing, and uninstalling Python packages behind the scenes. Even if the package is removed, the infection stays.

What is the damage?
This isn't just a bug; it is a full-scale credential stealer. It is designed to exfiltrate:
SSH Keys
Environment Variables (
.envfiles)Crypto Wallets
Cloud Tokens & K8s Secrets
How do I detect it?
I used Claude Code to research the specific indicators of compromise (IOCs) and generate detection scripts for both Windows and Linux. I am providing the full source for both here.
Linux & macOS Scanner
Save as: check-litellm-compromise.sh
#!/bin/bash
###############################################################################
# LiteLLM Supply Chain Compromise Scanner (Linux/macOS)
#
# Checks for indicators of compromise from the March 2026 TeamPCP attack
# on the litellm PyPI package (versions 1.82.7 and 1.82.8).
#
# The malicious package installs litellm_init.pth which executes on EVERY
# Python process startup and:
# - Steals SSH keys, cloud tokens, K8s secrets, crypto wallets, .env files
# - Installs a persistent systemd backdoor
# - Attempts lateral movement across Kubernetes clusters
#
# Usage:
# sudo bash check-litellm-compromise.sh # default: scan-dirs.json
# sudo bash check-litellm-compromise.sh my-dirs.json # custom config
###############################################################################
set -uo pipefail
CONFIG_FILE="${1:-$(dirname "$0")/scan-dirs.json}"
FOUND=0
LOGFILE="litellm-scan-$(date +%Y%m%d_%H%M%S).log"
log() {
echo "$1" | tee -a "$LOGFILE"
}
log "=== LITELLM COMPROMISE SCANNER (Linux/macOS) ==="
log "Date: $(date)"
log "Config: $CONFIG_FILE"
log "Log: $LOGFILE"
log ""
# Parse project dirs from JSON config
if [ ! -f "$CONFIG_FILE" ]; then
log "ERROR: Config file not found: $CONFIG_FILE"
log "Create one from the sample scan-dirs.json and add your project paths."
exit 1
fi
PROJECT_DIRS=()
while IFS= read -r dir; do
dir=$(echo "$dir" | sed 's/^[[:space:]]*"//;s/"[[:space:]]*,\?[[:space:]]*$//')
[ -n "$dir" ] && [[ ! "$dir" =~ ^_ ]] && PROJECT_DIRS+=("$dir")
done < <(grep -E '^\s*"/' "$CONFIG_FILE")
if [ ${#PROJECT_DIRS[@]} -eq 0 ]; then
log "WARNING: No directories found in $CONFIG_FILE"
log "Add your project paths to the project_dirs array."
fi
log "--- [1/11] Checking for litellm_init.pth (primary payload) ---"
RESULTS=$(find / -name "litellm_init.pth" 2>/dev/null)
if [ -n "$RESULTS" ]; then
log "!!! INFECTED !!! Malicious file found:"
log "$RESULTS"
FOUND=1
else
log "CLEAN: litellm_init.pth not found"
fi
log ""
log "--- [2/11] Checking for litellm in Python site-packages ---"
LITELLM_DIRS=$(find /usr/lib /usr/local/lib /home/*/.local/lib -type d -name "litellm" -path "*/site-packages/*" 2>/dev/null)
if [ -n "$LITELLM_DIRS" ]; then
log "FOUND litellm installed at:"
echo "$LITELLM_DIRS" | while read -r dir; do
log " $dir"
METADATA=$(dirname "$dir")/litellm-*.dist-info/METADATA
if ls $METADATA 2>/dev/null 1>&2; then
VER=$(grep '^Version:' $METADATA 2>/dev/null | head -1)
log " $VER"
if echo "$VER" | grep -qE '1\.82\.[78]'; then
log " !!! CRITICAL: This is a COMPROMISED version !!!"
fi
fi
done
FOUND=1
else
log "CLEAN: litellm not installed in any system Python environment"
fi
log ""
log "--- [3/11] Checking .pth files for executable code ---"
SUSPICIOUS_PTH=$(find /usr/lib /usr/local/lib /home -name "*.pth" -exec grep -l 'import\|exec\|eval\|subprocess\|os\.system\|urllib\|requests' {} \; 2>/dev/null)
if [ -n "$SUSPICIOUS_PTH" ]; then
log "WARNING: .pth files with executable code:"
echo "$SUSPICIOUS_PTH" | while read -r f; do
log " $f"
log " --- contents ---"
cat "$f" 2>/dev/null | while read -r line; do log " $line"; done
done
FOUND=1
else
log "CLEAN: No suspicious .pth files"
fi
log ""
log "--- [4/11] Checking for suspicious systemd services ---"
log "Recently created services (last 30 days):"
find /etc/systemd/system /home/*/.config/systemd /run/systemd/system -name "*.service" -mtime -30 2>/dev/null | grep -v '/run/systemd/generator' | while read -r svc; do
log " $svc ($(stat -c '%y' "$svc" 2>/dev/null | cut -d. -f1))"
done
log ""
log "User-level systemd services:"
USERSVC=$(find /home/*/.config/systemd -name "*.service" 2>/dev/null)
if [ -n "$USERSVC" ]; then
echo "$USERSVC" | while read -r svc; do
log " $svc"
grep -i 'exec' "$svc" 2>/dev/null | head -3 | while read -r line; do log " $line"; done
done
else
log " None found"
fi
log ""
log "--- [5/11] Checking cron jobs ---"
CRON_FOUND=0
for user in $(cut -d: -f1 /etc/passwd 2>/dev/null); do
CRON=$(crontab -l -u "$user" 2>/dev/null | grep -v '^#' | grep -v '^$')
if [ -n "$CRON" ]; then
log " User $user:"
echo "$CRON" | while read -r line; do log " $line"; done
CRON_FOUND=1
fi
done
[ $CRON_FOUND -eq 0 ] && log " No cron jobs found"
log ""
log "--- [6/11] Checking SSH key integrity ---"
for homedir in /home/*; do
user=$(basename "$homedir")
if [ -d "$homedir/.ssh" ]; then
log " User: $user"
ls -la "$homedir/.ssh/" 2>/dev/null | while read -r line; do log " $line"; done
if [ -f "$homedir/.ssh/authorized_keys" ]; then
KEYCOUNT=$(wc -l < "$homedir/.ssh/authorized_keys")
log " authorized_keys: $KEYCOUNT entries"
fi
fi
done
log ""
log "--- [7/11] Checking outbound connections ---"
OUTBOUND=$(ss -tnp 2>/dev/null | grep ESTAB | grep -v '127.0.0.1\|::1\|192.168\.\|10\.\|172\.1[6-9]\.\|172\.2[0-9]\.\|172\.3[0-1]\.')
if [ -n "$OUTBOUND" ]; then
log "Non-local outbound connections:"
echo "$OUTBOUND" | while read -r line; do log " $line"; done
else
log " No suspicious outbound connections"
fi
log ""
log "--- [8/11] Checking .env files ---"
find /home -name ".env" -type f 2>/dev/null | while read -r envfile; do
log " $envfile (modified: $(stat -c '%y' "$envfile" 2>/dev/null | cut -d. -f1))"
done
log ""
log "--- [9/11] Checking Docker for litellm ---"
if command -v docker &>/dev/null; then
DOCKER_MATCH=$(docker ps -a --format "{{.Names}} {{.Image}}" 2>/dev/null | grep -i litellm)
if [ -n "$DOCKER_MATCH" ]; then
log " FOUND litellm containers:"
echo "$DOCKER_MATCH" | while read -r line; do log " $line"; done
FOUND=1
else
log " No litellm containers"
fi
else
log " Docker not installed"
fi
log ""
log "--- [10/11] Checking pip logs for litellm history ---"
for homedir in /home/* /root; do
PIPLOGS=$(find "$homedir/.cache/pip" -name "*.log" 2>/dev/null)
if [ -n "$PIPLOGS" ]; then
MATCH=$(grep -li litellm $PIPLOGS 2>/dev/null)
if [ -n "$MATCH" ]; then
log " FOUND litellm in pip logs for $(basename "$homedir"):"
echo "$MATCH" | while read -r f; do
log " $f"
grep -i litellm "$f" 2>/dev/null | tail -5 | while read -r line; do log " $line"; done
done
FOUND=1
fi
fi
done
log ""
log "--- [11/11] Scanning project directories for litellm dependencies ---"
for projdir in "${PROJECT_DIRS[@]}"; do
if [ -d "$projdir" ]; then
log " Scanning: $projdir"
DIR_CLEAN=1
REQFILES=$(find "$projdir" -maxdepth 5 \( -name "requirements*.txt" -o -name "constraints*.txt" \) -exec grep -li 'litellm' {} \; 2>/dev/null)
if [ -n "$REQFILES" ]; then
log " FOUND in requirements files:"
echo "$REQFILES" | while read -r f; do
log " $f"
grep -i litellm "$f" | while read -r line; do log " $line"; done
done
FOUND=1; DIR_CLEAN=0
fi
PYPROJECTS=$(find "$projdir" -maxdepth 5 -name "pyproject.toml" -exec grep -li 'litellm' {} \; 2>/dev/null)
if [ -n "$PYPROJECTS" ]; then
log " FOUND in pyproject.toml:"
echo "$PYPROJECTS" | while read -r f; do
log " $f"
grep -i litellm "$f" | while read -r line; do log " $line"; done
done
FOUND=1; DIR_CLEAN=0
fi
SETUPFILES=$(find "$projdir" -maxdepth 5 \( -name "setup.py" -o -name "setup.cfg" \) -exec grep -li 'litellm' {} \; 2>/dev/null)
if [ -n "$SETUPFILES" ]; then
log " FOUND in setup files:"
echo "$SETUPFILES" | while read -r f; do
log " $f"
grep -i litellm "$f" | while read -r line; do log " $line"; done
done
FOUND=1; DIR_CLEAN=0
fi
PIPFILES=$(find "$projdir" -maxdepth 5 -name "Pipfile" -exec grep -li 'litellm' {} \; 2>/dev/null)
if [ -n "$PIPFILES" ]; then
log " FOUND in Pipfile:"
echo "$PIPFILES" | while read -r f; do
log " $f"
grep -i litellm "$f" | while read -r line; do log " $line"; done
done
FOUND=1; DIR_CLEAN=0
fi
PKGJSON=$(find "$projdir" -maxdepth 5 -name "package.json" -not -path "*/node_modules/*" -exec grep -li 'litellm' {} \; 2>/dev/null)
if [ -n "$PKGJSON" ]; then
log " FOUND in package.json:"
echo "$PKGJSON" | while read -r f; do
log " $f"
grep -i litellm "$f" | while read -r line; do log " $line"; done
done
FOUND=1; DIR_CLEAN=0
fi
DOCKERFILES=$(find "$projdir" -maxdepth 5 \( -name "docker-compose*.yml" -o -name "docker-compose*.yaml" -o -name "Dockerfile" -o -name "Dockerfile.*" \) -exec grep -li 'litellm' {} \; 2>/dev/null)
if [ -n "$DOCKERFILES" ]; then
log " FOUND in Docker files:"
echo "$DOCKERFILES" | while read -r f; do
log " $f"
grep -i litellm "$f" | while read -r line; do log " $line"; done
done
FOUND=1; DIR_CLEAN=0
fi
VENVS=$(find "$projdir" -maxdepth 5 -type d -name "litellm" -path "*/site-packages/*" 2>/dev/null)
if [ -n "$VENVS" ]; then
log " FOUND litellm in virtual environments:"
echo "$VENVS" | while read -r vdir; do
log " $vdir"
VMETA=$(dirname "$vdir")/litellm-*.dist-info/METADATA
if ls $VMETA 2>/dev/null 1>&2; then
VER=$(grep '^Version:' $VMETA 2>/dev/null | head -1)
log " $VER"
if echo "$VER" | grep -qE '1\.82\.[78]'; then
log " !!! CRITICAL: COMPROMISED VERSION !!!"
fi
fi
done
FOUND=1; DIR_CLEAN=0
fi
VENV_PTH=$(find "$projdir" -maxdepth 6 -name "litellm_init.pth" 2>/dev/null)
if [ -n "$VENV_PTH" ]; then
log " !!! INFECTED !!! Malicious .pth in project:"
echo "$VENV_PTH" | while read -r f; do log " $f"; done
FOUND=1; DIR_CLEAN=0
fi
[ $DIR_CLEAN -eq 1 ] && log " CLEAN"
else
log " SKIPPED (not found): $projdir"
fi
done
log ""
log "==========================================="
if [ $FOUND -eq 0 ]; then
log " VERDICT: CLEAN"
log " No indicators of compromise found."
log "==========================================="
else
log " VERDICT: REVIEW REQUIRED"
log "==========================================="
log ""
log "Findings detected. Review the output above."
log "If litellm versions 1.82.7 or 1.82.8 were EVER installed:"
log " 1. Rotate ALL credentials (SSH keys, API tokens, cloud keys, DB passwords)"
log " 2. Remove any unknown systemd services"
log " 3. Audit outbound network connections"
log " 4. Check for unauthorized authorized_keys entries"
log " 5. Treat this machine as potentially compromised"
fi
log ""
log "Full log saved to: $LOGFILE"
Configuration File: scan-dirs.json
(Place in the same directory as the script.)
{
"_comment": "Add your project directories here. The scanner will check each for litellm indicators.",
"project_dirs": [
"/home/youruser/Projects",
"/home/youruser/Documents/Projects",
"/opt/apps",
"/var/www"
]
}
Windows Scanner
Save as: check-litellm-compromise.ps1
###############################################################################
# LiteLLM Supply Chain Compromise Scanner (Windows)
#
# Checks for indicators of compromise from the March 2026 TeamPCP attack
# on the litellm PyPI package (versions 1.82.7 and 1.82.8).
###############################################################################
param(
[string]$ConfigFile = "$PSScriptRoot\scan-dirs-windows.json"
)
$ErrorActionPreference = "SilentlyContinue"
$Found = 0
$LogFile = "litellm-scan-$(Get-Date -Format 'yyyyMMdd_HHmmss').log"
function Log($msg) {
Write-Host $msg
Add-Content -Path $LogFile -Value $msg
}
Log "=== LITELLM COMPROMISE SCANNER (Windows) ==="
Log "Date: $(Get-Date)"
Log "Config: $ConfigFile"
Log "Log: $LogFile"
Log ""
if (-not (Test-Path $ConfigFile)) {
Log "ERROR: Config file not found: $ConfigFile"
Log "Create one from the sample scan-dirs-windows.json and add your project paths."
exit 1
}
$Config = Get-Content $ConfigFile | ConvertFrom-Json
$ProjectDirs = $Config.project_dirs
Log "--- [1/9] Checking for litellm_init.pth (primary payload) ---"
$PthResults = @()
$PythonPaths = @(
"$env:LOCALAPPDATA\Programs\Python",
"$env:APPDATA\Python",
"$env:ProgramFiles\Python*",
"$env:ProgramFiles(x86)\Python*",
"$env:USERPROFILE\.conda",
"$env:USERPROFILE\anaconda3",
"$env:USERPROFILE\miniconda3",
"$env:USERPROFILE\AppData\Local\pip"
)
foreach ($path in $PythonPaths) {
if (Test-Path $path) {
$results = Get-ChildItem -Path $path -Recurse -Filter "litellm_init.pth" -ErrorAction SilentlyContinue
$PthResults += $results
}
}
foreach ($drive in (Get-PSDrive -PSProvider FileSystem | Where-Object { $_.Used -gt 0 })) {
$results = Get-ChildItem -Path "$($drive.Root)" -Recurse -Filter "litellm_init.pth" -Depth 10 -ErrorAction SilentlyContinue
$PthResults += $results
}
if ($PthResults.Count -gt 0) {
Log "!!! INFECTED !!! Malicious file found:"
foreach ($f in $PthResults) { Log " $($f.FullName)" }
$Found = 1
} else {
Log "CLEAN: litellm_init.pth not found"
}
Log ""
Log "--- [2/9] Checking for litellm in Python environments ---"
$LitellmDirs = @()
foreach ($path in $PythonPaths) {
if (Test-Path $path) {
$results = Get-ChildItem -Path $path -Recurse -Directory -Filter "litellm" -ErrorAction SilentlyContinue |
Where-Object { $_.FullName -match "site-packages" }
$LitellmDirs += $results
}
}
if ($LitellmDirs.Count -gt 0) {
Log "FOUND litellm installed:"
foreach ($dir in $LitellmDirs) {
Log " $($dir.FullName)"
$metadata = Get-ChildItem -Path $dir.Parent.FullName -Filter "litellm-*.dist-info" -Directory -ErrorAction SilentlyContinue
if ($metadata) {
$metaFile = Join-Path $metadata.FullName "METADATA"
if (Test-Path $metaFile) {
$version = (Get-Content $metaFile | Select-String "^Version:").Line
Log " $version"
if ($version -match "1\.82\.[78]") { Log " !!! CRITICAL: This is a COMPROMISED version !!!" }
}
}
}
$Found = 1
} else {
Log "CLEAN: litellm not found in Python environments"
}
Log ""
Log "--- [3/9] Checking .pth files for executable code ---"
$SuspiciousPth = @()
foreach ($path in $PythonPaths) {
if (Test-Path $path) {
$pthFiles = Get-ChildItem -Path $path -Recurse -Filter "*.pth" -ErrorAction SilentlyContinue
foreach ($pth in $pthFiles) {
$content = Get-Content $pth.FullName -ErrorAction SilentlyContinue
if ($content -match "import|exec|eval|subprocess|os\.system|urllib|requests") {
$SuspiciousPth += $pth
}
}
}
}
if ($SuspiciousPth.Count -gt 0) {
Log "WARNING: .pth files with executable code:"
foreach ($f in $SuspiciousPth) {
Log " $($f.FullName)"
Log " --- contents ---"
Get-Content $f.FullName | ForEach-Object { Log " $_" }
}
$Found = 1
} else {
Log "CLEAN: No suspicious .pth files"
}
Log ""
Log "--- [4/9] Checking for suspicious services ---"
Log "Recently created services (last 30 days):"
$RecentDate = (Get-Date).AddDays(-30)
Get-WmiObject Win32_Service -ErrorAction SilentlyContinue |
Where-Object { $_.PathName -and $_.StartMode -eq "Auto" } |
ForEach-Object {
$exePath = ($_.PathName -replace '"','').Split(' ')[0]
if ((Test-Path $exePath) -and ((Get-Item $exePath).CreationTime -gt $RecentDate)) {
Log " $($_.Name): $($_.PathName) (created: $((Get-Item $exePath).CreationTime))"
}
}
Log ""
Log "--- [5/9] Checking scheduled tasks ---"
Get-ScheduledTask -ErrorAction SilentlyContinue |
Where-Object { $_.State -eq "Ready" -and $_.Author -notmatch "Microsoft|Windows" } |
ForEach-Object {
$actions = ($_.Actions | ForEach-Object { "$($_.Execute) $($_.Arguments)" }) -join "; "
Log " $($_.TaskName): $actions"
}
Log ""
Log "--- [6/9] Checking SSH keys ---"
$SshDir = "$env:USERPROFILE\.ssh"
if (Test-Path $SshDir) {
Get-ChildItem $SshDir -ErrorAction SilentlyContinue | ForEach-Object { Log " $($_.Name) (modified: $($_.LastWriteTime))" }
$AuthKeys = Join-Path $SshDir "authorized_keys"
if (Test-Path $AuthKeys) {
$count = (Get-Content $AuthKeys | Measure-Object -Line).Lines
Log " authorized_keys: $count entries"
}
} else {
Log " No .ssh directory found"
}
Log ""
Log "--- [7/9] Checking .env files ---"
$EnvFiles = Get-ChildItem -Path $env:USERPROFILE -Recurse -Filter ".env" -File -Depth 5 -ErrorAction SilentlyContinue
foreach ($envfile in $EnvFiles) { Log " $($envfile.FullName) (modified: $($envfile.LastWriteTime))" }
Log ""
Log "--- [8/9] Checking Docker for litellm ---"
if (Get-Command docker -ErrorAction SilentlyContinue) {
$dockerMatch = docker ps -a --format "{{.Names}} {{.Image}}" 2>$null | Select-String "litellm"
if ($dockerMatch) {
Log " FOUND litellm containers:"
foreach ($line in $dockerMatch) { Log " $line" }
$Found = 1
} else { Log " No litellm containers" }
} else { Log " Docker not installed" }
Log ""
Log "--- [9/9] Scanning project directories for litellm dependencies ---"
foreach ($projdir in $ProjectDirs) {
if (Test-Path $projdir) {
Log " Scanning: $projdir"
$DirClean = $true
$depPatterns = @(
@{ Name = "requirements files"; Filters = @("requirements*.txt", "constraints*.txt") },
@{ Name = "pyproject.toml"; Filters = @("pyproject.toml") },
@{ Name = "setup files"; Filters = @("setup.py", "setup.cfg") },
@{ Name = "Pipfile"; Filters = @("Pipfile") },
@{ Name = "package.json"; Filters = @("package.json") },
@{ Name = "Docker files"; Filters = @("docker-compose*.yml", "docker-compose*.yaml", "Dockerfile", "Dockerfile.*") }
)
foreach ($pattern in $depPatterns) {
foreach ($filter in $pattern.Filters) {
$files = Get-ChildItem -Path $projdir -Recurse -Filter $filter -Depth 5 -ErrorAction SilentlyContinue |
Where-Object { $_.FullName -notmatch "node_modules" } |
Where-Object { (Get-Content $_.FullName -ErrorAction SilentlyContinue) -match "litellm" }
foreach ($f in $files) {
Log " FOUND in $($pattern.Name): $($f.FullName)"
Get-Content $f.FullName | Select-String "litellm" | ForEach-Object { Log " $($_.Line.Trim())" }
$Found = 1; $DirClean = $false
}
}
}
$venvLitellm = Get-ChildItem -Path $projdir -Recurse -Directory -Filter "litellm" -Depth 6 -ErrorAction SilentlyContinue |
Where-Object { $_.FullName -match "site-packages" }
foreach ($vdir in $venvLitellm) {
Log " FOUND in virtual environment: $($vdir.FullName)"
$metadata = Get-ChildItem -Path $vdir.Parent.FullName -Filter "litellm-*.dist-info" -Directory -ErrorAction SilentlyContinue
if ($metadata) {
$metaFile = Join-Path $metadata.FullName "METADATA"
if (Test-Path $metaFile) {
$ver = (Get-Content $metaFile | Select-String "^Version:").Line
Log " $ver"
if ($ver -match "1\.82\.[78]") { Log " !!! CRITICAL: COMPROMISED VERSION !!!" }
}
}
$Found = 1; $DirClean = $false
}
$venvPth = Get-ChildItem -Path $projdir -Recurse -Filter "litellm_init.pth" -Depth 6 -ErrorAction SilentlyContinue
foreach ($pth in $venvPth) {
Log " !!! INFECTED !!! Malicious .pth in project: $($pth.FullName)"
$Found = 1; $DirClean = $false
}
if ($DirClean) { Log " CLEAN" }
} else { Log " SKIPPED (not found): $projdir" }
}
Log ""
Log "==========================================="
if ($Found -eq 0) {
Log " VERDICT: CLEAN"
Log " No indicators of compromise found."
Log "==========================================="
} else {
Log " VERDICT: REVIEW REQUIRED"
Log "==========================================="
Log ""
Log "Findings detected. Review the output above."
Log "If litellm versions 1.82.7 or 1.82.8 were EVER installed:"
Log " 1. Rotate ALL credentials (SSH keys, API tokens, cloud keys, DB passwords)"
Log " 2. Remove any unknown services or scheduled tasks"
Log " 3. Audit outbound network connections"
Log " 4. Check for unauthorized authorized_keys entries"
Log " 5. Treat this machine as potentially compromised"
}
Log ""
Log "Full log saved to: $LogFile"
Configuration File: scan-dirs-windows.json
{
"_comment": "Add your project directories here. The scanner will check each one for litellm indicators.",
"project_dirs": [
"C:\\Users\\YourUser\\Projects",
"C:\\Users\\YourUser\\Documents\\Projects",
"C:\\dev",
"D:\\Projects"
]
}
What to do if you are infected
If the scanner returns a "REVIEW REQUIRED" verdict or identifies versions 1.82.7 or 1.82.8:
Rotate ALL credentials immediately: This includes SSH keys, API tokens (OpenAI, Anthropic, AWS), and database passwords.
Audit
authorized_keys: Look for any public keys you did not add yourself.Wipe and Reinstall: In supply chain attacks involving persistence (like malicious
.pthfiles), the safest path is to re-image the machine or environment.