The problem
A package gets to execute on your machine at two points, and neither of them is under review:
- The build. A source distribution ships a
setup.pythat pip executes to generate metadata and build the wheel. That code runs duringpip install, with your user, your environment variables and your network. - The import. A wheel installs without executing anything, but the code runs the first time your program imports it, which for a dependency you actually use is moments later.
- Neither, and still executed. A
.pthfile dropped intosite-packagesis executed by the interpreter at startup. The litellm compromise (March 2026) used exactly that, so the payload ran on every interpreter launch whether the package was imported or not.
Most of the well documented PyPI compromises fired at import rather than at install. The
PyTorch advisory for torchtriton
states it directly: the malicious binary ran when the package was imported. So the useful control
is not "inspect it after installing" and not "trust wheels", it is decide before the artifact is
on the machine at all.
extuno is that decision, expressed as a command.
Install
pip install extuno
Python 3.9 or newer. Pure Python, no compiled extensions, no third-party dependencies. A tool that exists to reduce supply-chain exposure should not widen it, so the dependency list is empty and the CI job asserts it stays empty.
Quick start
1. Install the CLI
$ pip install extuno
Successfully installed extuno-0.1.2
2. Get an API key
Sign in at extuno.com and create a key under Profile → API keys (new accounts start with five scan credits, no card).
3. Authenticate once
$ extuno login --key extk_xxxxxxxxxxxxxxxxxxxxxxxx
OK logged in to https://extuno.com
credentials stored in ~/.config/extuno/config.json (owner-readable only)
scan credits: 5
Prefer stdin so the key never reaches the process list or your shell history:
$ echo "$EXTUNO_KEY" | extuno login --key -
4. Install through the gate
$ extuno pip install six
- six 1.17.0 ........................ sha256 ff70335d468e7eb6
- checking Extuno ................... not analysed yet
- scan queued ....................... 1 credit 0738cabf
- downloading package ............... in the isolated environment
- dynamic analysis .................. running in the sandbox
- analysis complete
verdict CLEAN risk 0/100
OK verdicts acceptable, installing
Successfully installed six-1.17.0
A release the platform has already analysed is answered from cache, at no cost:
$ extuno pip install requests
- checking Extuno ................... already analysed, no credit used
verdict CLEAN risk 0/100
And a bad one never reaches pip:
$ extuno pip install <flagged-package>
- checking Extuno ................... listed in the malicious database
verdict MALICIOUS
malicious-package Listed in the OpenSSF malicious-packages dataset
BLOCKED 1 of 1 package(s) blocked
How the check works
┌─ known-malicious catalog ──── hit ──→ MALICIOUS (free, instant)
resolve version │
+ digest ──────────┼─ previous analysis of this ── hit ──→ verdict (free, instant)
(index metadata) │ exact version
│
└─ miss ──→ submit scan ──→ static rules
(1 credit) + sandbox execution ──→ verdict
+ source review
- Resolve. The exact version and artifact digest are read from the index's JSON metadata. Nothing is downloaded.
- Catalog. The release is checked against a catalog of packages already recorded as malicious, built from OSV / OpenSSF malicious-packages and other feeds. A hit costs nothing and returns immediately.
- Prior analysis. If that exact version has already been analysed, the stored verdict is returned. Also free, also immediate.
- Analysis. Otherwise the artifact is fetched and detonated server side, inside a network-segmented microVM that is destroyed after the run: static rules over the source, the install path executed under an audit hook with a decoy home directory, and a reading pass over the entry points.
- Decide. pip is invoked only when every requirement has an acceptable verdict.
Threat model for the tool itself
This tool handles hostile input by design, so its own behaviour is the first thing worth auditing. Every claim below is enforced in code, in this repository.
| Property | Where |
|---|---|
| The artifact is never downloaded, unpacked, imported or executed locally. Only index JSON is read. | extuno/pypi.py |
requirements.txt is parsed as text. pip's resolver is not used for the assessment, because resolving an sdist can execute its build backend. |
extuno/reqs.py |
A nested -r include cannot escape the first file's directory; only regular files are read (st_size is 0 for /dev/zero and /proc, so size alone is not a bound); reads are byte-limited. |
extuno/reqs.py |
| A line the parser cannot classify is reported by location, never by content, so a requirements file cannot pipe an arbitrary file into a build log. | extuno/cli.py |
Package names and versions are validated before they reach a URL or an argument list. A token starting with - is never classified as a package. |
extuno/names.py |
| Option values are enumerated explicitly when splitting a pip command line, so an option's value is never mistaken for a package (and the reverse). | extuno/pipargs.py |
| Every untrusted string is stripped of C0/C1 controls, zero-width characters and bidirectional overrides before printing. A hostile package name cannot drive your terminal. | extuno/render.py |
The API key lives in a 0600 file created privately, is never placed in a URL or an argument, and never appears in an error message. |
extuno/config.py |
HTTPS is required. Plain HTTP is accepted only for a genuine loopback host, resolved with urlsplit().hostname so http://127.0.0.1@evil.example does not qualify. |
extuno/api.py |
| pip is invoked with an argument list and no shell. | extuno/cli.py |
Anything the assessment cannot cover stops the install rather than passing through: a direct URL, a
local path, an editable, an index override, or a bare positional after --. Override that
deliberately with --allow-unassessed.
An --index-url inside a requirements file is reported instead of silently honoured. Assessing the
public artifact while pip installs a private one is precisely the dependency-confusion shape.
Verdicts
A risk score answers "how much is going on in this package". Standing at an install prompt you are asking something narrower: "will installing this hurt me". Conflating the two produces a gate that blocks serious libraries for shipping an integration test suite, and a gate that cries wolf is removed from the pipeline within a week.
| Verdict | Meaning | Blocks by default |
|---|---|---|
malicious |
In the known-malicious catalog, or analysis proved attack behaviour: code executing at install time, credentials read then transmitted, a decoded payload executed | yes |
suspicious |
A real attack technique that is dual-use on its own, so a person should look first | yes |
error |
The check could not complete: release missing from the index, scan failed or timed out, or the index now serves a different artifact than the one analysed | yes |
review |
Findings worth knowing, such as a leaked-looking secret or a vulnerable pattern, that say nothing about intent | no |
clean |
Neither | no |
Tune the threshold:
extuno check -r requirements.txt --fail-on malicious,suspicious,error,review,unknown
Commands
| Command | Purpose |
|---|---|
extuno pip install ... |
Assess, then hand off to pip when the verdict allows it |
extuno scan PKG... |
Assess without installing |
extuno check -r requirements.txt |
CI gate: assess, exit non-zero when blocked, never install |
extuno credits |
Remaining scan credits |
extuno whoami |
Endpoint, key fragment, balance |
extuno login / extuno logout |
Manage the stored key |
Useful flags:
--fail-on malicious,suspicious,error # blocking policy
--no-scan # use only what is already known, spend nothing
--json report.json # machine-readable report
--sarif extuno.sarif # SARIF 2.1.0 for inline annotations
--no-dynamic / --no-ai / --no-static # skip an analysis stage
--allow-unassessed # install even when some arguments cannot be assessed
--timeout 900 # seconds to wait for one scan
Your usual pip arguments pass straight through:
extuno pip install -U 'flask==3.0.0' --user
extuno pip install -r requirements.txt -c constraints.txt
Continuous integration
extuno check never installs anything. It assesses, writes machine-readable output, and sets an
exit code.
GitHub Actions
- name: Supply-chain gate
env:
EXTUNO_API_KEY: ${{ secrets.EXTUNO_API_KEY }}
run: |
pip install extuno
extuno check -r requirements.txt --sarif extuno.sarif --json report.json
- uses: github/codeql-action/upload-sarif@v3
if: always()
with:
sarif_file: extuno.sarif
Or use the published action, which installs the CLI for you:
- uses: Extuno/extuno-cli@v0.1.9
with:
api-key: ${{ secrets.EXTUNO_API_KEY }}
requirements: requirements.txt
A tag is a moving target, so pin the action to a commit when the build has to be reproducible. This is the same advice the gate gives you about your own dependencies:
- uses: Extuno/extuno-cli@COMMIT_SHA # v0.1.9
Every input reaches the shell through the environment rather than through template interpolation,
so a value that arrives from a pull request title or an issue body cannot become shell syntax.
The action exposes verdict (clean, blocked, out-of-credits, error) and sarif-file as
step outputs, and it fails closed: an unreachable endpoint or an expired key stops the build
instead of waving it through.
GitLab CI
supply-chain:
image: python:3.12-slim
variables:
EXTUNO_API_KEY: $EXTUNO_API_KEY
script:
- pip install extuno
- extuno check -r requirements.txt --json report.json
artifacts:
when: always
paths: [report.json]
pre-commit
repos:
- repo: https://github.com/Extuno/extuno-cli
rev: v0.1.9
hooks:
- id: extuno
The default hook runs with --no-scan, so it answers from what Extuno already knows and spends
nothing. Use id: extuno-scan when an unseen release should be analysed on the spot.
Exit codes
| Code | Meaning |
|---|---|
0 |
Nothing blocked |
1 |
A package was blocked by policy |
2 |
Usage, network or server error |
3 |
Out of scan credits |
4 |
A scan did not finish in time |
EXTUNO_API_KEY and EXTUNO_API_URL are read from the environment, so CI never needs login.
How this differs from an advisory scanner
They answer different questions and work well together.
| Reported release | Unreported release | Stops the install | |
|---|---|---|---|
| Vulnerability scanning (SCA) | yes | no | no |
| Advisory-backed malware check | yes | no | warns |
| Reputation heuristics | partly | partly | yes |
| extuno | yes | yes, by analysing it | yes |
pip-audit matches installed versions against vulnerability records and
states it is not a static code analyser. Astral's uv audit
adds a malware check and documents its own boundary: the check
"relies on public advisories", so a release that has not been
reported yet has nothing to match against. That window, between publication and disclosure, is what
analysing the artifact covers.
Keep your advisory scanner for the CVEs in what you already run. Put this in front of what you are about to add.
Agent-driven installs
A coding agent that writes and then runs pip install removes the review step this ecosystem
quietly relied on: a person reading the package name before pressing enter.
Language models invent dependency names. A USENIX Security 2025 study generated 576,000 code samples across 16 models and found 205,474 unique hallucinated package names, at 5.2 percent of recommendations from commercial models and 21.7 percent from open ones. Attackers register the names that recur, so an invented import resolves to a real, hostile package. The paper's own recommendation is a two-step flow in which the proposed install is confirmed before it runs.
Point the agent at extuno pip install instead of pip install and that confirmation becomes
mechanical rather than aspirational.
Credits and cost
Checking a release the platform already knows is free and immediate. Analysing a release for the
first time costs one scan credit; new accounts start with five. A run stops with exit code 3
rather than half-finishing when the balance runs out.
extuno check -r requirements.txt --no-scan # spends nothing, uses only what is known
Development
git clone https://github.com/Extuno/extuno-cli
cd extuno-cli
pip install -e .
python -m pytest -q
The test suite covers the security-relevant behaviour directly: terminal escape stripping, requirements-file containment, pip argument splitting, credential file permissions and the TLS rules. Contributions are welcome; see CONTRIBUTING.md, and please read SECURITY.md before reporting a vulnerability.
Private indexes
If you pass --index-url or --extra-index-url, the tool says so plainly: versions are resolved
against the public index, so the artifact pip installs from a private index may not be the artifact
that was analysed. That gap is the shape of a dependency-confusion attack, and it should be a
decision rather than a surprise.
Links
- Package: pypi.org/project/extuno
- Documentation: extuno.com/pip-scanner
- Malicious package database: extuno.com/malicious-db
- Changelog: CHANGELOG.md
MIT licensed. Built by extuno.com · Tolga SEZER.
Release files for extuno 0.1.9
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| extuno-0.1.9.tar.gz | 44.9 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| extuno-0.1.9-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 81.1 kB
Release files / extuno-0.1.9.tar.gz
| Download URL | extuno-0.1.9.tar.gz |
|---|---|
| Size | 44.9 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
b534c0127aea7d8a30041bfc0679d376a86177433803d8b0e5d622bd3800a677
|
|
BLAKE2b-256 checksum How to use checksums |
4113008629e9f35a6076e340c53eaa08d5175d40e4e68082bc1baefd11fe59e1
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.12.3
|
Release files / extuno-0.1.9-py3-none-any.whl
| Download URL | extuno-0.1.9-py3-none-any.whl |
|---|---|
| Size | 36.2 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
b71af91a6ed9064aeb7a25c274e301153e6e580a73a507fb3f3ba4976ec90c3d
|
|
BLAKE2b-256 checksum How to use checksums |
2c4d9041020995a1e7aec1561587a197e75d2de60809e9155ba444c40fa6bf1e
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.12.3
|