Scans JavaScript and Python lockfiles for supply-chain compromises and known vulnerabilities.
Quick Start · Installation · CLI Usage · MCP Server · Data Sources
brew install undont/tap/supplyscan
supplyscan scan # scan current directory
supplyscan check lodash 4.17.20 # check an npm package
supplyscan check django 2.2.0 -e pypi # check a PyPI package- Supply-chain detection across npm and PyPI by aggregating IOCs from DataDog (Shai-Hulud v2 and TeamPCP / Mini Shai-Hulud), GitHub Advisory Database, and OSV.dev, matched per-ecosystem so same-named packages don't collide
- Vulnerability scanning via the npm audit API for npm and OSV.dev for PyPI
- Multi-format lockfile support across npm, Yarn (classic & berry), pnpm, Bun, Deno, and Python (pip, Poetry, Pipenv, uv, PDM)
- Heuristic advisories for packages that run install scripts and for invisible or non-ASCII characters in package names or URLs
- CLI and MCP modes in a single binary, switchable with
--mcp - JSON output for scripting and CI use
- Per-source caching with a configurable TTL, so each IOC source refreshes on its own schedule
| Package Manager | Lockfile |
|---|---|
| npm | package-lock.json, npm-shrinkwrap.json |
| Yarn Classic | yarn.lock (v1) |
| Yarn Berry | yarn.lock (v2+) |
| pnpm | pnpm-lock.yaml |
| Bun | bun.lock |
| Deno | deno.lock |
| pip | requirements.txt |
| Poetry | poetry.lock |
| Pipenv | Pipfile.lock |
| uv | uv.lock |
| PDM | pdm.lock |
Written in Go and shipped as a static binary, so the scanner itself can't be compromised by the package ecosystems it scans.
brew install undont/tap/supplyscango install github.com/undont/supplyscan/cmd/supplyscan@latestRequires Go 1.26+ and $GOPATH/bin in your PATH.
Download binary
Pre-built binaries are available from GitHub Releases:
# macOS (Apple Silicon)
curl -L https://github.com/undont/supplyscan/releases/latest/download/supplyscan-darwin-arm64 \
-o /usr/local/bin/supplyscan && chmod +x /usr/local/bin/supplyscan
# macOS (Intel)
curl -L https://github.com/undont/supplyscan/releases/latest/download/supplyscan-darwin-amd64 \
-o /usr/local/bin/supplyscan && chmod +x /usr/local/bin/supplyscan
# Linux (x64)
curl -L https://github.com/undont/supplyscan/releases/latest/download/supplyscan-linux-amd64 \
-o /usr/local/bin/supplyscan && chmod +x /usr/local/bin/supplyscanBuild from source
git clone https://github.com/undont/supplyscan.git
cd supplyscan
go build -o supplyscan ./cmd/supplyscan
mv supplyscan /usr/local/bin/The CLI is the default mode; no flags required.
# Scan current directory (recursive by default, scans every file in the tree)
supplyscan scan
supplyscan . # shorthand
# Scan a monorepo root; all sub-package lockfiles are found and audited
supplyscan scan /path/to/monorepo
# Scan only the top level, without descending into subdirectories
supplyscan scan /path/to/project --no-recursive # --shallow is an alias
# Scan production dependencies only (exclude devDependencies)
supplyscan scan --no-dev
# Fail (exit 3) when something can't be audited — unpinned requirements or a
# manifest with no lockfile — even if no compromise is found. Useful in CI.
supplyscan scan --strict
# Check a specific package (npm by default)
supplyscan check lodash 4.17.20
# Check a PyPI package
supplyscan check django 2.2.0 --ecosystem pypi
supplyscan check django 2.2.0 -e pypi # short form
# Refresh IOC database
supplyscan refresh
supplyscan refresh --force # force update even if cache is fresh
# Show status
supplyscan status
# Output raw JSON (for scripting/CI)
supplyscan scan --json
supplyscan check lodash 4.17.20 --json
# Show help
supplyscan help| Code | Meaning |
|---|---|
0 |
Clean — nothing found |
1 |
Error (bad path, etc.) |
2 |
Findings — a supply-chain compromise or known vulnerability |
3 |
Coverage gaps under --strict (something could not be audited; no findings) |
Coverage gaps are informational by default and do not change the exit code; only --strict turns them into the distinct exit code 3, and a real finding (exit 2) always takes precedence.
For AI agent integration (Claude Code, Cursor, etc.), supplyscan runs as an MCP server with the --mcp flag.
brew install undont/tap/supplyscan && \
claude mcp add mcp-supplyscan --transport stdio -s user -- supplyscan --mcpAdd to your MCP config file:
{
"mcpServers": {
"mcp-supplyscan": {
"command": "supplyscan",
"args": ["--mcp"]
}
}
}| Tool | Description |
|---|---|
supplyscan_status |
Scanner version, IOC database info, supported lockfiles |
supplyscan_scan |
Scan project directory for compromises and vulnerabilities |
supplyscan_check |
Check single package@version |
supplyscan_refresh |
Update IOC database from upstream sources |
Tool parameters
| Parameter | Type | Description |
|---|---|---|
path |
string | Path to the project directory |
include_dev |
boolean | Include dev dependencies (default: true) |
| Parameter | Type | Description |
|---|---|---|
package |
string | Package name |
version |
string | Package version |
ecosystem |
string | npm (default) or pypi |
| Parameter | Type | Description |
|---|---|---|
force |
boolean | Force refresh even if cache is fresh |
# Homebrew
brew upgrade supplyscan
# Go
go install github.com/undont/supplyscan/cmd/supplyscan@latestUse supplyscan status (CLI) or supplyscan_status (MCP) to check your current version.
- DataDog Indicators of Compromise — Shai-Hulud v2 for the original Shai-Hulud worm packages (npm)
- DataDog Indicators of Compromise — TeamPCP for the Mini Shai-Hulud / TeamPCP campaign (npm and PyPI rows)
- GitHub Advisory Database for npm and PyPI (
pip) malware advisories (GHSA) - OSV.dev for npm and PyPI malware entries (
MAL-advisories)
- npm audit API for known CVEs in npm packages
- OSV.dev querybatch for known vulnerabilities in PyPI packages
IOC entries expressed as version ranges (e.g. < 1.2.3, >= 1.0.0, < 2.0.0) are evaluated for npm using semver, so a range advisory matches any affected installed version. PyPI IOC ranges are not range-evaluated — PyPI matches exact pins, enumerated version lists, and all-versions wildcards only. This is because PyPI uses PEP 440, not semver, and adding a PEP 440 parser cuts against the tool's minimal-attack-surface design for a thin slice of cases (the overwhelming majority of PyPI malware IOCs are enumerated specific versions or all-versions typosquats). PEP 440 range support is a documented fast-follow if range-form PyPI IOCs start appearing.
