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)
| File | Size | Uploaded | |
|---|---|---|---|
| immunis-0.1.0.tar.gz | 21.4 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| 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 logRelease 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