Metadata-Version: 2.4
Name: extuno
Version: 0.1.9
Summary: Assess a Python package for supply-chain risk before pip installs it
Author: extuno.com Tolga SEZER
Maintainer: extuno.com Tolga SEZER
License: MIT
Project-URL: Homepage, https://extuno.com/pip-scanner
Project-URL: Documentation, https://extuno.com/pip-scanner
Project-URL: Source, https://github.com/Extuno/extuno-cli
Project-URL: Issues, https://github.com/Extuno/extuno-cli/issues
Project-URL: Changelog, https://github.com/Extuno/extuno-cli/blob/main/CHANGELOG.md
Keywords: security,supply-chain,pip,malware,sandbox,sbom,dependency-confusion,typosquatting,ci,devsecops
Classifier: Development Status :: 4 - Beta
Classifier: Environment :: Console
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: System Administrators
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Security
Classifier: Topic :: Software Development :: Quality Assurance
Classifier: Topic :: System :: Software Distribution
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
Dynamic: license-file

<div align="center">

<img src="docs/media/logo.svg" width="76" alt="Extuno">

# extuno

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

[![PyPI](https://img.shields.io/pypi/v/extuno?style=flat-square&logo=pypi&logoColor=white&label=PyPI&color=2FBFAE)](https://pypi.org/project/extuno/)
[![Python](https://img.shields.io/pypi/pyversions/extuno?style=flat-square&logo=python&logoColor=white&color=3776AB)](https://pypi.org/project/extuno/)
[![License](https://img.shields.io/badge/license-MIT-2FBFAE?style=flat-square)](LICENSE)
[![CI](https://img.shields.io/github/actions/workflow/status/Extuno/extuno-cli/ci.yml?style=flat-square&logo=githubactions&logoColor=white&label=CI)](https://github.com/Extuno/extuno-cli/actions)
[![Marketplace](https://img.shields.io/badge/GitHub%20Marketplace-listed-2088FF?style=flat-square&logo=githubactions&logoColor=white)](https://github.com/marketplace/actions/extuno-supply-chain-gate)
[![Dependencies](https://img.shields.io/badge/runtime%20dependencies-0-2FBFAE?style=flat-square)](pyproject.toml)

[![Linux](https://img.shields.io/badge/Linux-supported-FCC624?style=flat-square&logo=linux&logoColor=black)](#)
[![macOS](https://img.shields.io/badge/macOS-supported-000000?style=flat-square&logo=apple&logoColor=white)](#)
[![Windows](https://img.shields.io/badge/Windows-supported-0078D6?style=flat-square&logo=windows&logoColor=white)](#)
[![SARIF](https://img.shields.io/badge/output-SARIF%202.1.0-4B32C3?style=flat-square)](#continuous-integration)
[![GitHub Actions](https://img.shields.io/badge/CI-GitHub%20%7C%20GitLab%20%7C%20Azure-2088FF?style=flat-square&logo=githubactions&logoColor=white)](#continuous-integration)

<img src="docs/media/terminal.svg" width="820" alt="extuno pip install six returns a clean verdict and installs; a flagged package is blocked before pip is reached">

</div>

---

## The problem

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

<img src="docs/media/flow.svg" width="900" alt="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)](https://securitylabs.datadoghq.com/articles/litellm-compromised-pypi-teampcp-supply-chain-campaign/)
  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`](https://pytorch.org/blog/compromised-nightly-dependency/)
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

```console
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**

```console
$ pip install extuno
Successfully installed extuno-0.1.2
```

**2. Get an API key**

Sign in at [extuno.com](https://extuno.com/signup) and create a key under **Profile → API keys**
(new accounts start with five scan credits, no card).

**3. Authenticate once**

```console
$ 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:

```console
$ echo "$EXTUNO_KEY" | extuno login --key -
```

**4. Install through the gate**

```console
$ 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:

```console
$ extuno pip install requests
  - checking Extuno ................... already analysed, no credit used
  verdict CLEAN  risk 0/100
```

And a bad one never reaches pip:

```console
$ 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](https://github.com/ossf/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`](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`](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`](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`](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`](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`](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`](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`](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`](extuno/api.py) |
| pip is invoked with an argument list and no shell. | [`extuno/cli.py`](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:

```console
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:

```console
--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:

```console
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**

```yaml
- 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](https://github.com/marketplace/actions/extuno-supply-chain-gate),
which installs the CLI for you:

```yaml
- 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:

```yaml
- 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**

```yaml
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**

```yaml
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](https://pypi.org/project/pip-audit/). Astral's `uv audit`
adds a malware check and documents its own boundary: the check
["relies on public advisories"](https://astral.sh/blog/uv-audit), 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](https://arxiv.org/abs/2406.10279) 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.

```console
extuno check -r requirements.txt --no-scan   # spends nothing, uses only what is known
```

---

## Development

```console
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](CONTRIBUTING.md), and please read
[SECURITY.md](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](https://pypi.org/project/extuno/)
- Documentation: [extuno.com/pip-scanner](https://extuno.com/pip-scanner)
- Malicious package database: [extuno.com/malicious-db](https://extuno.com/malicious-db)
- Changelog: [CHANGELOG.md](CHANGELOG.md)

MIT licensed. Built by [extuno.com](https://extuno.com) &middot; Tolga SEZER.
