Security scanner for Python packages — detects supply chain attacks before they execute
Project description
chaincanary
Stop supply-chain attacks before they execute. The only scanner with semantic .pth analysis.
Caught LiteLLM 1.82.8 as MALICIOUS at publish time — before any advisory existed. No cloud. No account. Nothing leaves your machine.
Try it now — zero install:
pipx run chaincanary check requests==2.28.0
Or scan the real LiteLLM attack (uses local mock wheel — no network):
git clone https://github.com/AetherCore-Dev/chaincanary && cd chaincanary
pip install -e .
chaincanary check litellm==1.82.8 --local tests/fixtures/litellm-1.82.8-py3-none-any.whl
Why developers use chaincanary:
- Catches what others miss — semantic
.pthclassifier detects attacks that run on every Python startup, not just at install time - Zero config —
pip install chaincanary && chaincanary check <package>, done - Nothing leaves your machine — pure offline static analysis, no cloud, no account, no proxy
- CI-ready — GitHub Action blocks malicious packages on every push
- Audit entire projects — scan all dependencies in
requirements.txtorpyproject.tomlin one command - Version diffing — see exactly what changed between releases
- Works in China — no proxy needed, unlike socket.dev / Safety
Install
pip install chaincanary
Or try without installing: pipx run chaincanary check <package>
Usage
Scan a single package
chaincanary check requests==2.28.0
chaincanary check litellm==1.82.8
# Scan a local .whl file (no network needed)
chaincanary check litellm==1.82.8 --local ./litellm-1.82.8-py3-none-any.whl
Audit your entire project
chaincanary audit requirements.txt
chaincanary audit pyproject.toml --fail-on HIGH_RISK
Compare two versions
chaincanary diff litellm 1.82.6 1.82.8
Safe install (scan before installing)
chaincanary install requests==2.32.0
chaincanary install litellm==1.82.8 # blocked — MALICIOUS
JSON output (for pipelines)
chaincanary check litellm==1.82.8 --json-output | jq '.verdict'
# "MALICIOUS"
SARIF output (for GitHub Code Scanning)
# Single package → SARIF
chaincanary check litellm==1.82.8 --local ./litellm-1.82.8-py3-none-any.whl --sarif-output > results.sarif
# Audit lockfile → SARIF
chaincanary audit requirements.txt --sarif-output > chaincanary.sarif
Upload to GitHub Security tab in your CI workflow:
- name: Upload SARIF
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: chaincanary.sarif
Offline mode (air-gapped / CI cache)
# Download wheels first (standard pip)
pip download -r requirements.txt -d ./wheels/
# Scan without any network calls
chaincanary audit requirements.txt --offline --wheel-dir ./wheels/
# Single package offline
chaincanary check mypackage==1.0.0 --offline --local ./mypackage-1.0.0-py3-none-any.whl
GitHub Action — drop-in CI protection
# .github/workflows/security.yml
name: Supply Chain Security
on: [push, pull_request]
jobs:
scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: AetherCore-Dev/chaincanary@v0.1.0
with:
requirements: requirements.txt
fail-on: MALICIOUS # or HIGH_RISK for stricter mode
sarif-output: chaincanary.sarif # optional: upload to GitHub Security tab
- name: Upload SARIF
if: always()
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: chaincanary.sarif
Fails your build if any dependency matches a known attack pattern. Zero config.
Why chaincanary catches what others miss
A .pth file in Python's site-packages runs on every Python startup — not just at install time. Other scanners only check setup.py install hooks and miss this entirely.
chaincanary has a semantic .pth classifier with 4 categories:
| .pth content | Classification | Finding |
|---|---|---|
| Empty | Normal | silent |
/usr/local/lib/... |
Path-only | silent |
| setuptools distutils shim | Safe code | LOW |
subprocess.Popen(['curl', ...]) |
Dangerous | CRITICAL |
LiteLLM 1.82.8 was flagged MALICIOUS by chaincanary at publish time. Other tools missed it entirely, or flagged it only after manual rule updates.
What chaincanary detects
| Attack vector | Location | Severity |
|---|---|---|
.pth file with network/subprocess |
site-packages/*.pth |
CRITICAL |
Network call during pip install |
setup.py |
HIGH |
Shell command during pip install |
setup.py |
HIGH |
exec(base64.decode(...)) obfuscation |
anywhere | HIGH |
DNS exfiltration via socket.getaddrinfo |
__init__.py |
HIGH |
Network call on every import |
__init__.py |
MEDIUM |
| SSH / AWS credential access | anywhere | HIGH |
| Path traversal in wheel zip | .whl structure |
CRITICAL |
| Known malicious SHA256 hash | .whl file |
CRITICAL |
| Typosquatting (Levenshtein distance) | package name | MEDIUM |
Comparison
| chaincanary | pip-audit | Trivy | socket.dev | Safety | |
|---|---|---|---|---|---|
.pth semantic analysis |
4-category classifier | -- | -- | no static classifier | -- |
| Detects LiteLLM 1.82.8 at publish time | offline, no rules needed | -- | -- | only after manual rule update | -- |
| No account / cloud needed | yes | yes | yes | no (GitHub App) | no (account) |
| Nothing leaves your machine | yes | yes | yes | no (uploads metadata) | yes |
| Offline capable | yes | partial | yes | no | no |
| China mainland access (no proxy) | yes | yes | yes | no (403) | no (403) |
| Open source | yes | yes | yes | no (SaaS) | partial |
chaincanary is not a CVE scanner — use it alongside
pip-auditfor vulnerability advisory coverage.
The LiteLLM attack — what happened
March 24, 2026: TeamPCP hijacked LiteLLM on PyPI
Threat actor TeamPCP hijacked the LiteLLM maintainer's PyPI account and published two malicious versions:
| Version | Attack vector | Trigger |
|---|---|---|
| 1.82.7 | Payload injected into litellm/proxy/proxy_server.py |
import litellm.proxy |
| 1.82.8 | Hidden .pth file (litellm_init.pth, 34 KB) |
Every Python startup — no import needed |
The .pth attack in 1.82.8:
# litellm_init.pth — executes on every Python startup, silently, forever
import os, subprocess, sys
subprocess.Popen([sys.executable, "-c", "import base64; exec(base64.b64decode('...'))"])
The payload collects SSH keys, env vars, AWS/GCP/K8s credentials, crypto wallets, CI secrets — encrypts with AES-256 + RSA-4096 and exfiltrates to https://models.litellm.cloud/ (a fake domain registered the day before).
This runs every time you start Python — not just during pip install. LiteLLM was downloaded ~95 million times per month. chaincanary flagged both versions MALICIOUS at publish time — without any advisory, rule update, or cloud lookup.
How it works
chaincanary check <package>
|
v
Download wheel (no install, no execute)
|
v
Static analysis:
- Zip safety (path traversal, zip bomb)
- .pth semantic classifier (4 categories)
- AST analysis of setup.py / install hooks
- Obfuscation detection (base64, eval, exec)
- __init__.py delayed-trigger scan
- DNS exfiltration patterns
- SHA256 malicious hash database
- Typosquatting distance check
|
v
Score 0-10 -> Verdict: SAFE / LOW_RISK / HIGH_RISK / MALICIOUS
|
v
Block or proceed
No sandboxing. No Docker. No kernel modules. Pure Python static analysis in seconds.
Add chaincanary badge to your project
Show that your project is scanned for supply-chain attacks:
[](https://github.com/AetherCore-Dev/chaincanary)
Known Limitations
chaincanary is a static behavioral scanner, not a magic bullet:
| Gap | Mitigation |
|---|---|
No C extension analysis (.so/.pyd) |
Sandbox mode in v0.3 |
| No CVE database | Use alongside pip-audit |
| No dynamic sandbox | Static signals only (for now) |
| Multi-stage payloads (download at runtime) | Runtime monitoring in v0.3 |
Roadmap
| Version | Theme | Status |
|---|---|---|
| v0.1 | Core engine: .pth classifier, audit, diff, GitHub Action |
shipped |
| v0.2 | SARIF output, offline mode, hash feed, pre-commit hook | in progress |
| v0.3 | Lightweight sandbox, package reputation, npm/cargo | later |
Full plan: ROADMAP.md
Contributing
git clone https://github.com/AetherCore-Dev/chaincanary
cd chaincanary
pip install -e ".[dev]"
pytest tests/
PRs welcome — especially:
- New malicious hash signatures
- Detection rules for new attack patterns
- False positive reports
License
Apache 2.0. See LICENSE.
Built after the LiteLLM supply chain attack, March 2026.
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file chaincanary-0.2.0.tar.gz.
File metadata
- Download URL: chaincanary-0.2.0.tar.gz
- Upload date:
- Size: 75.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
b93e9230b7742e8ed5fffc83f9425eb853891ba8990f0bc3335133c0f56a2968
|
|
| MD5 |
55af4e5057c1ecbf3640332315dd34b7
|
|
| BLAKE2b-256 |
97cf15f3b127b46d0fe4e2b0843e6153a30cc13c0ae1d85fd4282bbe6c3c2566
|
Provenance
The following attestation bundles were made for chaincanary-0.2.0.tar.gz:
Publisher:
release.yml on AetherCore-Dev/chaincanary
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
chaincanary-0.2.0.tar.gz -
Subject digest:
b93e9230b7742e8ed5fffc83f9425eb853891ba8990f0bc3335133c0f56a2968 - Sigstore transparency entry: 1187221245
- Sigstore integration time:
-
Permalink:
AetherCore-Dev/chaincanary@1f64939e0f159813225865a13d19282bd2982cf1 -
Branch / Tag:
refs/tags/v0.2.0 - Owner: https://github.com/AetherCore-Dev
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@1f64939e0f159813225865a13d19282bd2982cf1 -
Trigger Event:
push
-
Statement type:
File details
Details for the file chaincanary-0.2.0-py3-none-any.whl.
File metadata
- Download URL: chaincanary-0.2.0-py3-none-any.whl
- Upload date:
- Size: 53.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
a8264b5a29034d926d6d8c6ae2a571742024c994be1497e24011605062cd11b2
|
|
| MD5 |
086de3969318691a6c0dc3f41835e00f
|
|
| BLAKE2b-256 |
c3f81fd9a3ec77be4d8145f25c87fc34e0bc758e96952569b8626be27db72940
|
Provenance
The following attestation bundles were made for chaincanary-0.2.0-py3-none-any.whl:
Publisher:
release.yml on AetherCore-Dev/chaincanary
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
chaincanary-0.2.0-py3-none-any.whl -
Subject digest:
a8264b5a29034d926d6d8c6ae2a571742024c994be1497e24011605062cd11b2 - Sigstore transparency entry: 1187221306
- Sigstore integration time:
-
Permalink:
AetherCore-Dev/chaincanary@1f64939e0f159813225865a13d19282bd2982cf1 -
Branch / Tag:
refs/tags/v0.2.0 - Owner: https://github.com/AetherCore-Dev
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@1f64939e0f159813225865a13d19282bd2982cf1 -
Trigger Event:
push
-
Statement type: