Skip to main content
Extuno

extuno

Assess a Python package for supply-chain risk before pip installs it.

PyPI Python License CI Marketplace Dependencies

Linux macOS Windows SARIF GitHub Actions

extuno pip install six returns a clean verdict and installs; a flagged package is blocked before pip is reached

The problem

A package gets to execute on your machine at two points, and neither of them is under review:

Install timeline: resolve, download, extuno assessment, then the two execution points, the source build and the first import
  • The build. A source distribution ships a setup.py that pip executes to generate metadata and build the wheel. That code runs during pip 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 .pth file dropped into site-packages is 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
  1. Resolve. The exact version and artifact digest are read from the index's JSON metadata. Nothing is downloaded.
  2. 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.
  3. Prior analysis. If that exact version has already been analysed, the stored verdict is returned. Also free, also immediate.
  4. 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.
  5. 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

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)

Source distribution for extuno 0.1.9
File Size Uploaded
extuno-0.1.9.tar.gz 44.9 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for extuno 0.1.9
File Interpreter ABI Platform
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

Release history Release notifications | RSS feed

This release

0.1.9 This release

2 release files

0.1.8

2 release files

0.1.7

2 release files

0.1.6

2 release files

0.1.5

2 release files

0.1.4

2 release files

0.1.3

2 release files

0.1.2

2 release files

0.1.1

2 release files

0.1.0

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