Skip to content

Usage

pipguard demo

Basic Usage

Install a single package

pipguard install requests

pipguard will download, scan, and install the package from the scanned local cache. By default, the report is summary-first: CRITICAL / HIGH / MEDIUM findings are expanded, LOW findings are collapsed to package-level counts, and CLEAN packages are shown only in the summary totals.

Install from requirements.txt

pipguard install -r requirements.txt

Scans all packages in the file. Blocks on first CRITICAL or HIGH finding.

Supported Dependency Sources

pipguard can only guarantee its scan promise for artifacts it downloads and inspects, so it accepts requirement entries that resolve to a fixed, verifiable artifact and rejects (exit 2) the ones that don't:

Requirement form Accepted?
PyPI specifier (requests==2.31.0, numpy>=1.24)
Hash-locked (pkg==1.0 --hash=sha256:…)
Pinned VCS (git+https://…@<commit-sha>) ✅ (commit SHA required)
Direct URL with hash (pkg @ https://…/pkg-1.0.whl#sha256=…) ✅ (hash required)
Editable (-e .) ⏭️ skipped with a warning
Unpinned VCS (git+https://…, no commit) ❌ exit 2
Direct URL without a hash ❌ exit 2
Local path (./pkg, /abs/pkg) ❌ exit 2

Use --require-hashes (or [install] require_hashes = true in a policy file) to require a hash on every entry — the same integrity guarantee as pip install --require-hashes. VCS and direct-URL support can be disabled entirely via the allow_vcs_pinned / allow_direct_url_pinned policy keys.

CI Mode

In CI, you never want interactive prompts. Use --yes to suppress all confirmation prompts and have pipguard exit 1 automatically on CRITICAL or HIGH findings:

pipguard install --yes -r requirements.txt

Default Output

Successful installs keep raw pip install logs quiet unless you opt in with --show-pip-output.

$ pipguard install some-package
📦 Downloading to /tmp/pipguard-abcd1234 ...
🔍 Scanning 5 package(s) ...
Scan summary:
  Total packages: 5
  CRITICAL: 0  HIGH: 0  MEDIUM: 1  LOW: 2  CLEAN: 2

MEDIUM
  [MEDIUM] jsonschema
    [MEDIUM] jsonschema/validators.py:113
           Outbound network call (urllib.request.urlopen()) in runtime code

LOW
  [LOW] markupsafe — 1 finding
  [LOW] zipp — 1 finding
  Use --verbose to show LOW-level file details.

Proceed with installation? [y/N]
GitHub Actions
$pipguard install --yes -r requirements.txt
 
✓ requests==2.31.0
✓ numpy==1.26.3
✗ litellm==1.82.8 CRITICAL — .pth autorun, reads ~/.ssh/id_rsa
 
Process exited with code 1

Known CVE Lookup (osv.dev)

pipguard's AST scanner detects suspicious behaviour. The --check-vulns flag adds a complementary signal by querying osv.dev for published vulnerabilities in each package version:

pipguard install --check-vulns requests

Known CVEs appear in a dedicated Known CVEs (osv.dev) section — shown even for packages the behavioural scan marks CLEAN:

Known CVEs (osv.dev) — 1 CVE
  jinja2==3.1.5
    CVE-2024-56326 [MEDIUM] Jinja sandbox breakout through attr filter selection (fixed in 3.1.6)

CVEs are informational by default and do not block the install. To make them a hard gate (exit 1), add --fail-on-vuln (which implies --check-vulns):

pipguard install --fail-on-vuln -r requirements.txt

Opt-in network call

OSV lookups are the only outbound request pipguard makes during a scan, so they are opt-in. Without --check-vulns (or [osv] enabled = true in a policy file) pipguard stays offline. The lookup is best-effort — if osv.dev is unreachable it fails open and the behavioural scan still runs.

Policy-file equivalent:

[osv]
enabled = true       # same as --check-vulns
fail_on_vuln = false # set true for --fail-on-vuln

Scanning the PyPI Feed (Reporter Workflow)

pipguard scan-feed watches PyPI's RSS feed of recent releases, scans each entry without installing anything, and surfaces the high-risk ones as candidates for manual review. This operationalizes the "reporter" workflow — most new releases are boring; a scan lets one person focus attention on the few that look suspicious, and file an advisory if a manual look confirms it.

# Scan the 20 most recent releases; list anything HIGH or CRITICAL
pipguard scan-feed

# New packages (not just new releases of existing ones), CRITICAL-only
pipguard scan-feed --feed packages --min-level critical

# Scan a saved feed file, widen the net to MEDIUM, add CVE lookups
pipguard scan-feed --feed ./updates.xml --min-level medium --check-vulns

Options: --feed (updates (default) | packages | a URL or local file), --limit N (default 20; 0 = no limit), --min-level (critical|high|medium|low, default high), plus --allow, --check-vulns, --verbose, and --policy.

Exit codes: 1 if any package meets or exceeds --min-level (so a scheduled job can alert), 0 if none do, 2 on a feed/download error. sdist-only releases are skipped (they can't be scanned without executing build code).

This is triage, not proof

A flagged package is a candidate for review, not a confirmed attack. Inspect it (e.g. via the PyPI Inspector) before acting.

Run it as a scheduled sentinel

The exit-1-on-candidates behavior makes scan-feed easy to schedule. The repo ships a ready-to-copy GitHub Actions workflow at examples/scan-feed-cron.yml that runs on a cron and opens a GitHub issue whenever releases are flagged:

cp examples/scan-feed-cron.yml .github/workflows/pipguard-sentinel.yml

Allowing Known-Legitimate Packages

Some packages legitimately access credential stores (e.g. paramiko reads ~/.ssh). Use --allow to reduce their finding from HIGH to MEDIUM:

pipguard install --allow paramiko paramiko

CRITICAL findings are never reduced

--allow only reduces HIGH → MEDIUM. CRITICAL findings always block, regardless of flags.

Forcing a Package (Escape Hatch)

For known false-positives on fully-trusted internal packages:

pipguard install --force my-trusted-internal-pkg

Use with care

--force bypasses all checks and logs a warning. Never use in CI without code review.

Allowing sdist Packages

By default pipguard exits with code 2 if a package falls back to sdist (source distribution), because sdists execute build scripts. To opt in:

pipguard install --allow-sdist some-package

sdist installs execute arbitrary code

--allow-sdist bypasses a hard safety boundary. Even though pipguard runs AST scanning on setup.py, pip install will still execute setup.py and any build-backend code at install time. pipguard's AST scan does NOT prevent this. Never use --allow-sdist in automated pipelines without explicit review.

All Flags

Flag Description
-r FILE Install from requirements file
--yes / -y CI mode — no prompts, exit 1 on CRITICAL/HIGH
--allow PKG Add package to per-invocation allowlist (HIGH→MEDIUM)
--force PKG Bypass all checks for a specific package
--allow-sdist Allow sdist fallback (DANGER: executes arbitrary code — AST scan does NOT prevent this)
--require-hashes Require hash-locked requirements entries (--hash=... or URL hash fragment)
--verbose Show full scan details, including LOW findings and CLEAN package list
--show-pip-output Show raw pip install output instead of the quiet default
--sandbox Run the install step under a capability sandbox (experimental) — install-time code (e.g. an sdist build) can't read credential paths or open network connections. See Runtime Sandbox.
--policy FILE Load policy file (default: ./pipguard.toml if present)
--intel-feed FILE_OR_URL Threat-intel JSON feed containing blocked package versions
--enforce-intel Enforce intel feed denylist and block matching packages
--check-vulns Query OSV.dev for known vulnerabilities (opt-in network call; informational)
--fail-on-vuln Exit 1 if any package has a known OSV vulnerability (implies --check-vulns)

Exit Codes

Code Meaning
0 Clean — all packages installed
1 Blocked — CRITICAL or HIGH risk detected
2 Scan error — download failed or unsupported format