Supply Chain Firewall for pip, npm, and 6 more ecosystems.
Zero dependencies. Python 3.8+. Safe on infected machines.
Updating a compromised package doesn't make you safe. The backdoor already ran. Your keys are already stolen. litellm_init.pth is still on your disk auto-executing on every Python startup.
That's the Ghost Gap — the damage that patching doesn't fix. This tool closes it.
curl -sSL https://raw.githubusercontent.com/Vezran/ghostgap/main/ghostgap-safe.sh | bashThis is bash, not Python. It quarantines litellm_init.pth using system tools (find/grep/mv), then launches Python with -S (skips all .pth processing). The malware never runs. Not even once.
pip install ghostgap
ghostgap assess # Am I safe?
ghostgap cure # Fix everythingWhy two methods? Python
.pthfiles auto-run during interpreter startup at the C level (Py_InitializeFromConfig->site.main()->addpackage()-> line 213), BEFORE any user code. No Python tool — ours or anyone else's — can prevent this.ghostgap-safe.shuses bash +python -Sto bypass.pthentirely. The pip-installedghostgapcommand cannot prevent.pthexecution in its own process — by the timemain()runs, all.pthcode has already executed. Use the pip command on machines you know are clean, or to assess/cure after running the safe bootstrap.
You: pip install litellm --upgrade # "I'm safe now, right?"
Reality: sysmon.py already stole your SSH keys, AWS creds, and K8s config.
litellm_init.pth is still in site-packages — runs on EVERY Python startup.
A rogue pod is still running in kube-system.
Your CI/CD secrets were exfiltrated during the pip install.
Updating the package didn't undo any of that.
LiteLLM's official fix: "delete some files and rotate keys manually."
Arthur's community scripts: "scan your CI logs" (but their Python scanner loads the malicious .pth on startup — the LiteLLM CEO acknowledged this).
ghostgap: one command, finds everything, fixes everything, doesn't trigger the malware.
| Problem | Others | Ghost Gap |
|---|---|---|
.pth persistence survives pip uninstall |
Most scanners don't check .pth files. Python-based scanners auto-load the malware on startup | Uses system find — never loads .pth. Scans AND deletes them |
| Transitive dependencies (dspy, agent frameworks) | "Check if you installed litellm" | ghostgap scan-all checks every installed package |
| CI/CD pipeline exposure | Separate GitHub/GitLab gist scripts | ghostgap ci-scan github <org> / gitlab <group> built in |
| Credential rotation | "Rotate all secrets manually" | Auto-rotates SSH, AWS, GCP, Azure, K8s, Git, Docker, HF, Terraform |
| K8s rogue pods | Not mentioned in most advisories | Auto-scans kube-system, detects and reports suspicious pods |
| Ongoing protection | Run once, forget | ghostgap protect hooks into every pip install automatically |
| C2 domain detection | IOC list to check manually | Scans .pth files and source for models.litellm.cloud / checkmarx.zone |
| Multi-ecosystem | Python only (or npm only) | Python, npm, Ruby, Rust, Go, Java, PHP, Docker |
pip install ghostgapOr install from source:
pip install git+https://github.com/Vezran/ghostgap.gitghostgap assess # Ghost Gap check — am I safe?
ghostgap scan-all # Check ALL installed packages (catches transitive deps)
ghostgap deep-scan # Scan ALL Python envs (Homebrew, pyenv, conda, venvs, pipx)
ghostgap feed # Show all 23 known threatsghostgap cure # Full remediation (detect + remove + rotate + verify)
ghostgap cure litellm # Target specific packageghostgap install requests # Scan then install (Python)
ghostgap npm-install express # Scan then install (Node.js)
ghostgap check litellm 1.82.7 # Check without installing
ghostgap check event-stream 3.3.6 --npm
ghostgap protect # Auto-protect every pip install
ghostgap unprotect # Remove auto-protectionghostgap scan requirements.txt # Python (requirements)
ghostgap scan pyproject.toml # Python (PEP 621 + Poetry)
ghostgap scan Pipfile # Python (Pipfile)
ghostgap scan Pipfile.lock # Python (Pipfile.lock)
ghostgap scan package.json # Node.js
ghostgap scan package-lock.json # Node.js (v1/v2/v3 lockfiles)
ghostgap scan Gemfile # Ruby
ghostgap scan Cargo.toml # Rust
ghostgap scan go.mod # Go
ghostgap scan pom.xml # Java (Maven)
ghostgap scan build.gradle # Java (Gradle)
ghostgap scan composer.json # PHP
ghostgap scan composer.lock # PHP (lockfile)
ghostgap scan Dockerfile # Docker (FROM tags + curl|bash)
ghostgap ci requirements.txt # CI/CD gate (exit 0=clean, 1=blocked)
ghostgap ci Dockerfile --strict # CI/CD gate (--strict blocks REVIEW verdicts too)ghostgap ci-scan github <org> # Scan GitHub Actions logs
ghostgap ci-scan gitlab <group> # Scan GitLab CI logsDrop this into .github/workflows/ghostgap.yml:
name: Ghost Gap
on:
pull_request:
paths:
- 'requirements*.txt'
- 'setup.py'
- 'setup.cfg'
- 'pyproject.toml'
- 'Pipfile'
- 'Pipfile.lock'
- 'package.json'
- 'package-lock.json'
- 'yarn.lock'
- 'pnpm-lock.yaml'
push:
branches: [main, master]
paths:
- 'requirements*.txt'
- 'pyproject.toml'
- 'package.json'
- 'package-lock.json'
jobs:
supply-chain-check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: pip install ghostgap
- name: Scan Python dependencies
run: |
for f in requirements*.txt; do
[ -f "$f" ] && ghostgap ci "$f"
done
- name: Scan npm dependencies
if: hashFiles('package.json') != ''
run: ghostgap ci package.json
- run: ghostgap assess| Package | Versions | Threat | Actor |
|---|---|---|---|
| litellm | 1.82.7, 1.82.8 | Supply chain attack via Trivy CI/CD compromise. Credential theft + .pth persistence + K8s pod injection | TeamPCP |
| litellm | 1.82.0 - 1.82.2 | Guardrail logging bug leaked API keys in spend logs and OTEL traces | software bug |
| ultralytics | 8.3.41, 8.3.42 | Cryptominer via compromised GitHub Actions | unknown |
| ctx | 0.1.2, 0.2.0 | AWS credential exfiltration via env vars | unknown |
| phpass | 0.9.99 | Dependency confusion stealing env vars | unknown |
| Package | Versions | Threat | Actor |
|---|---|---|---|
| event-stream | 3.3.6 | Crypto wallet theft via flatmap-stream | right9ctrl |
| ua-parser-js | 0.7.29, 0.8.0, 1.0.0 | Cryptominer + password stealer via hijacked maintainer | unknown |
| colors | 1.4.1, 1.4.2 | Protestware (infinite loop) | Marak |
| node-ipc | 10.1.1 - 10.1.3 | Protestware (file overwrite on Russian/Belarusian IPs) | RIAEvangelist |
| coa | 2.0.3+ | Credential theft via hijacked preinstall | unknown |
| rc | 1.2.9, 1.3.0, 2.3.9 | Credential theft (hijacked alongside coa) | unknown |
| es5-ext | 0.10.63 | Protestware (anti-war message on Russian IPs) | medikoo |
| @lottiefiles/lottie-player | 2.0.8 | Crypto wallet drainer | unknown |
| Package | Versions | Threat | Actor |
|---|---|---|---|
| rest-client | 1.6.13 | Hijacked gem exfiltrated env vars | unknown |
| strong_password | 0.0.7 | Backdoor downloaded and eval'd code from pastebin | unknown |
| bootstrap-sass | 3.2.0.3 | Backdoor via cookie-based arbitrary code execution | unknown |
| Package | Versions | Threat | Actor |
|---|---|---|---|
| rustdecimal | 1.23.1 | Typosquat of rust_decimal, stole env vars via Telegram | unknown |
| cratesio | 0.1.0 | Typosquat attempting to steal crates.io API tokens | unknown |
| Package | Versions | Threat | Actor |
|---|---|---|---|
| log4j-core | 2.0 - 2.16.0 | Log4Shell (CVE-2021-44228) — remote code execution via JNDI injection | CVE-2021-44228 |
| protobuf-java | 3.16.0 | Dependency confusion attack on Google protobuf | unknown |
| Package | Versions | Threat | Actor |
|---|---|---|---|
| phpunit/phpunit | 4.8.28, 5.6.3 | Backdoor in eval-stdin.php allowing remote code execution | unknown |
| Image | Threat |
|---|---|
| Various Docker Hub images | Compromised with cryptominers and backdoors. ghostgap flags unpinned FROM tags and curl|bash in RUN instructions |
.pth files in Python's site-packages are auto-executed on every Python startup. Here's the exact CPython code that does it (Lib/site.py, line 212-213):
if line.startswith(("import ", "import\t")):
exec(line)The litellm 1.82.8 attack dropped litellm_init.pth which means:
- Even after
pip uninstall litellm, the.pthfile stays in site-packages - Every time you run
python, it runs the malicious code - Any Python-based scanner you run on the infected machine triggers the malware first
- This includes pip-audit, safety, Snyk, and the Arthur community scripts
ghostgap-safe.sh is a bash script (not Python) that:
- Uses
find/grep/mvto quarantine malicious.pthfiles — no Python involved - Launches Python with
-Sflag (skipssite.pyentirely, so.pthfiles never load) - Runs a safe
.pthparser that processes path entries (for namespace packages) but blocks all executable lines - Runs
ghostgap curein this clean Python process
The result: .pth malware never runs. Not even once. Not in a throwaway process. Not anywhere.
The pip-installed ghostgap command does NOT have this property. When you run ghostgap via pip, Python starts normally, site.py processes all .pth files, and the malware executes before main() is called. The pip command will detect and warn about malicious .pth files, but the damage is already done. Always use ghostgap-safe.sh on potentially infected machines.
The execution chain is: Py_InitializeFromConfig() (C) -> import site -> site.main() -> addsitepackages() -> addsitedir() -> addpackage() -> exec(line). This happens at the interpreter level before any user Python code runs. There is no Python-level mechanism to intercept or prevent .pth code execution. The -S flag is the only built-in defense, and our safe .pth parser is the only way to get site-packages imports working without .pth code execution.
When you run ghostgap check <package> <version>, it downloads the package, extracts it, and scans every file for:
- Credential file access patterns — references to
~/.ssh/id_*,~/.aws/credentials,~/.kube/config, etc. - Code obfuscation —
exec(compile(...)),eval(base64.b64decode(...)),marshal.loads(),__import__('base64') - Data exfiltration indicators — outbound HTTP calls, socket connections, aiohttp/httpx usage
- Malicious npm install scripts —
preinstall/postinstallhooks running curl/wget/node - Base64-encoded payloads — long base64 strings decoded and checked for credential keywords
The verdict logic minimizes false positives: credential access alone is common in cloud tools (boto3, litellm, awscli) and doesn't trigger a block. Only credential access combined with obfuscation triggers a block — the actual attack pattern.
ghostgap cure rotates:
| Service | What it does |
|---|---|
| SSH | Generates new ed25519 key, backs up and removes old keys |
| AWS | Creates new IAM access key, deactivates old one, updates ~/.aws/credentials |
| GCP | Removes compromised ADC, prompts re-auth |
| Azure | Removes all Azure token JSON files, backs up |
| Kubernetes | Backs up ~/.kube/config, prompts re-gen for EKS/GKE/AKS |
| Git | Removes ~/.git-credentials, prompts token revocation |
| GitHub CLI | Removes ~/.config/gh/hosts.yml, prompts gh auth login |
| Docker | Clears auth tokens from ~/.docker/config.json |
| HuggingFace | Removes cached tokens |
| Terraform | Removes Terraform Cloud credentials |
from ghostgap import SupplyChainFirewall, Ecosystem, Verdict
fw = SupplyChainFirewall()
# Am I safe?
gap = fw.ghost_gap_assess()
print("Safe:", gap.safe)
print("Exposed credentials:", len(gap.exposed_credentials))
# Cure an infection
result = fw.cure()
print("Was infected:", result.was_infected)
print("System clean:", result.system_clean)
print("Credentials rotated:", result.credentials_rotated)
# Scan before install
v = fw.scan_before_install("litellm", "1.82.7")
assert v.verdict == Verdict.BLOCK
# Scan across ecosystems
v = fw.scan_before_install("event-stream", "3.3.6", Ecosystem.NODEJS)
assert v.verdict == Verdict.BLOCK
v = fw.scan_before_install("rest-client", "1.6.13", Ecosystem.RUBY)
assert v.verdict == Verdict.BLOCK
# Scan a manifest
report = fw.scan_manifest("requirements.txt")
print("Blocked:", report.blocked)
# Scan all installed packages
hits = fw.scan_installed()
# CI/CD gate
exit_code = fw.ci_gate("requirements.txt") # 0=clean, 1=blocked
exit_code = fw.ci_gate("Dockerfile", strict=True) # also blocks REVIEW verdicts
# Deep filesystem scan (all Python environments)
hits = fw.deep_scan_filesystem()
# CI/CD pipeline scanning
github_hits = fw.scan_ci_github("my-org", token="ghp_...")
gitlab_hits = fw.scan_ci_gitlab("my-group", token="glpat-...")
# Add custom threats to the feed
from ghostgap import ThreatRecord, ThreatCategory
fw.threat_feed.add(ThreatRecord(
package="evil-package",
ecosystem=Ecosystem.PYTHON,
bad_versions=["0.1.0"],
safe_version="N/A",
threat_category=ThreatCategory.BACKDOOR,
actor="attacker",
description="Custom threat",
))| Feature | ghostgap | pip-audit | safety | Snyk |
|---|---|---|---|---|
| Ghost Gap (post-infection cleanup) | Yes | No | No | No |
| Cure command | Yes | No | No | No |
| Auto credential rotation | Yes | No | No | No |
| Safe on infected machines | Yes (bash bootstrap only) | No | No | No |
| .pth persistence detection | Yes | No | No | No |
| K8s rogue pod detection | Yes | No | No | No |
| CI/CD pipeline scanning | Yes | No | No | No |
| 8 ecosystems in one tool | Yes | pip only | pip only | Yes |
| Manifest scanning (14 formats) | Yes | pip only | pip only | Yes |
| Deep source code scan | Yes | No | No | Limited |
| Dockerfile scanning | Yes | No | No | Yes |
| Auto-protect pip installs | Yes | No | No | No |
| Zero dependencies | Yes | No | No | No |
| Free and open source | Yes | Yes | Freemium | Freemium |
Tested against 67 manifest files across 5 ecosystems in a real production codebase (16 microservices, multiple languages):
| Ecosystem | Files | Clean | Blocked | False Positives |
|---|---|---|---|---|
| Python | 15 | 15 | 0 | 0 |
| Node.js | 9 | 9 | 0 | 0 |
| Rust | 15 | 15 | 0 | 0 |
| Go | 6 | 6 | 0 | 0 |
| Docker | 22 | 21 | 1 (true positive) | 0 |
| Total | 67 | 66 | 1 | 0 |
163/163 unit tests passed across all 8 ecosystems (6 test files, 163 tests covering threat feed, scanning, manifest parsing, verdict logic, CLI, launcher, CI scanning, and the .pth safety proof).
ghostgap uses only Python stdlib. No requests, no click, no rich, no pydantic. pip install ghostgap adds exactly one package to your environment.
For a supply chain security tool, this isn't a feature — it's a requirement. Every dependency is an attack surface.
Found a compromised package we don't cover? Open an issue or PR with the threat record:
ThreatRecord(
package="package-name",
ecosystem=Ecosystem.PYTHON, # or NODEJS, RUBY, RUST, GO, JAVA, PHP, DOCKER
bad_versions=["1.0.0"],
safe_version="0.9.0",
threat_category=ThreatCategory.CREDENTIAL_THEFT, # or BACKDOOR, CRYPTOMINER, etc.
actor="attacker-name",
description="What the attack does",
source="Advisory URL",
)Apache-2.0
Vezran x taazbro
Closing the Ghost Gap — because updating doesn't undo the damage.