Skip to main content

immunis

Find a Python repository's dependency-confusion exposure.

pip install immunis
immunis scan .

The problem

pip resolves a requirement across every index it can see and takes the highest version it finds. Point it at a private index with --extra-index-url and the public index is still in scope — so anyone who registers one of your internal distribution names on pypi.org, with a higher version number, gets their code installed instead of yours. In your build. With your credentials in the environment.

The defence is an install where the private index is the only index in scope:

# public dependencies, public index
RUN pip install -r requirements.txt

# yours, private index only — no union, nothing to outrank
RUN pip install --index-url "$PRIVATE_INDEX" --no-deps -r requirements-private.txt

immunis finds the places a repository has quietly given that up.

What it checks

check needs config what it means
pip-union-mode no --extra-index-url in a Dockerfile, requirements file, pip.conf, setup.cfg or CI workflow — pip is resolving across indexes
credentials-in-url no a https://user:token@… URL committed to the tree
plaintext-env-secret no a tracked .env assigning a real credential value
private-dep-in-public-requirements yes one of your private distributions pinned in a file resolved against the public index
sops-single-recipient no a sops store with fewer recipients than it needs to be both usable and recoverable
required-scan-missing yes a CI workflow missing a scan the repository says it requires

Not what this does: known-bad detection. Secrets in git history, CVEs in dependencies, CVEs in built images — gitleaks, pip-audit and trivy answer those and answer them better. immunis answers "is this repository's package resolution still shaped the way you think it is?", which those tools cannot see.

Tracked, not merely present

Findings come from files git tracks. A gitignored deployment .env sitting in a working copy is the system working as designed, and reporting it as a committed credential is noise of the worst kind — alarming, wrong, and guaranteed to bury the real finding underneath it. Outside a git repository immunis falls back to walking the filesystem, because an unpacked tarball still deserves a scan.

Configuration

Optional, in immunis.toml or [tool.immunis] in pyproject.toml:

# Which distribution names are yours. immunis cannot infer this, and without it
# the private-dependency check stays silent.
private_distributions = ["acme-internal", "acme-widgets"]

fail_on = "high"          # default; "none" reports without gating
min_sops_recipients = 2
exclude = ["tests/fixtures/*"]

[[required_workflow_scans]]
path = ".github/workflows/ci.yml"
markers = ["gitleaks", "trivy"]

A check that needs configuration does nothing without it. A scanner that fires on repositories which never opted in gets switched off in a week, and then it is gating nothing at all.

Suppressing a finding

# immunis: allow credentials-in-url — fixture for the test that asserts we catch these
INDEX = "https://reader:abc123@pypi.example.com/simple/"

Covers that check on the same line or the line below; for a finding that names a path but no line, anywhere in that file. The check id is required — there is deliberately no blanket allow, because a repository should not be able to switch the scanner off by accident, and a suppression you have to name is one a reviewer can argue with.

Prefer restructuring over suppressing. immunis's own credential fixture is assembled from pieces rather than waived.

In CI

- name: Dependency-confusion scan
  run: |
    python -m venv /tmp/immunis && /tmp/immunis/bin/pip install -q immunis
    /tmp/immunis/bin/immunis scan . --fail-on high

Exit codes: 0 clean, 1 findings at or above --fail-on, 2 the scan could not run. --json writes the scan to stdout; --report URL POSTs it to a collector (bearer from IMMUNIS_TOKEN), best-effort — a collector being down must not turn your pipeline red, so the exit code comes from the findings alone.

No runtime dependencies

This is a security property, not minimalism. A scanner is often installed with the private index as the only index in scope — pip install --index-url <private> --no-deps immunis — precisely so that no public package can substitute itself for the tool that checks for substitution. A dependency here would force --extra-index-url back into that install line. Config is read with stdlib tomllib.

License

MIT.

Release files for immunis 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for immunis 0.1.0
File Size Uploaded
immunis-0.1.0.tar.gz 21.4 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for immunis 0.1.0
File Interpreter ABI Platform
immunis-0.1.0-py3-none-any.whl Python 3 none any Details

Total release size: 39.2 kB

Release files / immunis-0.1.0.tar.gz

Download URL immunis-0.1.0.tar.gz
Size 21.4 kB
Tags Source
SHA-256 checksum
How to use checksums
3ef280e2f0b0f1f4562be82d7be3f0e57695111f2e47c9b8491512b27d6118ba
BLAKE2b-256 checksum
How to use checksums
385fc261ecf602922e1e564d62f4e988f948d678310019d649e04f1e16eb444c
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 24, 2026.

Transparency log

Release files / immunis-0.1.0-py3-none-any.whl

Download URL immunis-0.1.0-py3-none-any.whl
Size 17.8 kB
Tags Python 3
SHA-256 checksum
How to use checksums
d29ea83ea55490090c4c8a62a3f57858402dc1abbefd949ba820c6918bde2f9d
BLAKE2b-256 checksum
How to use checksums
38ed608a354f58502bab62078dc86020247a6eb1ac631c4a828441cc1c179cf8
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 24, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page