v0.3.0 released — the control-plane release: signals, denylist & auto-approval

Clear every model
before it comes aboard.

The open-source model supply-chain control plane. Signals go in — Purser's own never-execute scanner, the Hub's upstream verdicts, verified signatures — and one policy verdict comes out, enforced in CI and at Kubernetes admission: restrict by country of origin, publisher, name, format, or signer identity.

Free & open source · Apache-2.0 · self-hosted · no telemetry

~35model formats
0bytes ever executed
1 verdictenforced in CI · API · K8s
Ed25519 + Sigstoresigned provenance
cosignsigned images + chart

Who it's for

Two teams, one clearance desk

Platform & security teams

Gate every model at the door — in CI, at the registry, or with a Kubernetes admission webhook. Set policy once (severity, formats, publishers, country of origin) and let the exit code fail the build.

Set up the gate →

ML engineers & data scientists

Vet a model you just pulled from Hugging Face before you load it. One command surfaces hidden code, exfiltration indicators, upstream scanner verdicts, and policy violations — without ever executing the model.

Check a model →

Signals

Signal one: best-of-breed scanning across ~35 formats

Scanning is one input to the verdict — and it's a strong one. Purser takes the techniques from open-source scanners (modelscan, picklescan) and extends them with an exfiltration engine and trust_remote_code analysis — all at the byte and opcode level. On Hub scans it also ingests HuggingFace's own upstream scan verdicts (picklescan, ClamAV, Protect AI, JFrog) as a corroborating signal. Treat a clean scan as necessary, not sufficient — that's why the verdict aggregates more than one signal.

Pickle & PyTorch

.pkl .pt .pth .bin .ckpt .joblib — dangerous imports via GLOBAL/STACK_GLOBAL, multi-pickle streams, REDUCE-on-load, evasive opcodes.

Keras / TensorFlow

Lambda & TFOpLambda bytecode layers, non-builtin custom layers (external code on load), PyFunc/EagerPyFunc graph ops, TFLite Flex delegates, file-access kernels.

ONNX & safetensors

Custom Python operator domains, external-data path traversal, and safetensors header validation against parser-bug exploits.

GGUF

Chat-template Jinja SSTI injection — __subclasses__, os. access, and dynamic code smuggled in templates.

Bundled Python

Full AST analysis of trust_remote_code: exec/eval, sockets & HTTP, dynamic import, marshal, and base64/hex deobfuscation.

Exfiltration engine

Webhooks, hard-coded IP:port, cloud/API secrets, and base64/hex/base32/base85 (+ gzip/zlib), UTF-16 wide-string, and single-byte-XOR payloads — decoded and re-analyzed.

pickle / PyTorch safetensors GGUF ONNX Keras .h5/.keras TFLite TF SavedModel TF.js trust_remote_code config.json auto_map TorchServe .mar MLflow Caffe CoreML OpenVINO IR ExecuTorch skops PaddlePaddle PMML NumPy .npy/.npz archives (zip/tar) NeMo H2O MOJO TensorRT GGML MXNet XGBoost / LightGBM Darknet Torch7 Flax msgpack

lit = dedicated scanner · dim = format-ID for policy + full exfiltration scan

Why Purser

One control plane: policy, provenance, and enforcement

Never executes a model

Pickle streams are parsed with pickletools.genops; archives via read-only member reads; configs via yaml.safe_load. The model is never loaded into a framework.

Policy by origin, publisher & name

YAML policy with severity thresholds and allow/blocklists for formats, publishers, model-name globs, and country of origin. Per-rule overrides; require_signed.

Verified provenance

Ed25519 model signing over a full-file manifest, checked against a trust store with revocation and validity windows — plus offline Sigstore (Fulcio/Rekor) verified-identity provenance from a transparency-log-backed external root. Verified origin outranks claimed origin.

Deploy anywhere

Ships as a CLI, a REST API, digest-pinned Wolfi container images, plain kustomize manifests, and a production Helm chart with hardened defaults.

Observability built in

Prometheus /metrics (verdicts, categories, formats, origins, latencies), a structured syslog/JSON audit log, and a Grafana dashboard (pictured below).

Signed supply chain

Hash-pinned lockfiles, CycloneDX SBOMs, THIRD_PARTY_LICENSES, cosign-keyless-signed images and chart, a CI vulnerability gate, and an OpenSSF Best Practices passing badge.

Signals in, one verdict out

External intelligence plugs into the same policy engine: upstream Hub scan verdicts (upstream unsafe corroborates; upstream safe never downgrades Purser's own finding), offline loader-CVE intel (a declared framework version with a known load-time RCE — refreshed weekly from OSV, and end users pull updates with purser update-intel), a refreshable known-bad denylist (content hashes + publisher/repo globs), an opt-in model-card / eval-attestation gate (a card is a claim, not proof), the deep-analysis companion, and your own sources via the purser.signals plugin entry point — with findings tagged to MITRE ATLAS techniques.

Enforced, not advisory

The verdict has teeth in three places: exit codes gate CI (plus a GitHub Action), the REST API gates registry/promotion hooks, and a fail-closed Kubernetes admission webhook rejects pods referencing unapproved model digests or unpinned images. The approved list can even maintain itself: a PASS approves the model's digest, a later FAIL revokes it — scan → approve → admit, no manual hop.

Observability

See every verdict, block, and threat category live

Prometheus /metrics plus a bundled Grafana dashboard (deploy/grafana/purser-overview.json): verdict rates, findings by threat category, policy blocks by reason, provenance status, country-of-origin and format mix, API rejections, throughput, and in-flight scans.

Purser Grafana dashboard showing scans-per-minute by verdict, findings by threat category, policy blocks by reason, provenance status, scans by country of origin, files scanned by format, API request rejections, throughput, and in-flight scans

In your pipeline

Gate models in CI or in the cluster

GitLab / GitHub CI

Run the image as a job; the exit code gates the build (0 pass · 1 findings · 2 blocked · 3 error).

scan-models:
  image: ghcr.io/purser-io/purser:0.3.0
  script:
    - purser scan ./models \
        --policy .purser/policy.yaml \
        --format sarif --output purser.sarif

Kubernetes admission control

Enable the validating webhook and Purser enforces at deploy time: images must be pinned by digest, and any model a workload declares must be on the approved-scan list. Fail-closed, opt-in per namespace.

helm upgrade purser \
  oci://ghcr.io/purser-io/charts/purser \
  --set admission.enabled=true
kubectl label ns my-app \
  purser.io/admission=enforce

How Purser compares

Where Purser sits next to the model scanners

These are scanners; Purser is the layer above them — it can consume their class of verdicts as one signal among several, then enforce the result. Best-effort assessment of publicly documented features (2026). Legend: ✅ yes · ◐ partial · ❌ no.

CapabilityPurserpicklescanFicklingModelScanModelAudit
Pickle opcode malware scan
Broad format coverage
Data-exfiltration engine
trust_remote_code AST
Upstream verdict ingestion + signal plugins
Policy: country / publisher / name
Signed provenance / trust store
REST API + K8s + Helm
Deploy-time enforcement (CI action + K8s admission)
Prometheus / audit observability
LicenseApache-2.0OSSOSSApache-2.0MIT

Detection and false-positive figures are backed by a reproducible benchmark — 0% false positives over 75 real HuggingFace models, re-measured weekly in CI. ModelAudit leads on raw format-scanner depth (40+ formats, e.g. TensorRT) and maturity; commercial platforms (incl. Protect AI Guardian, now Palo Alto Networks) add live threat feeds and behavioral backdoor detection Purser does not attempt — and no verdict here means "certified safe."

Install

Up and running in a minute

helm install purser oci://ghcr.io/purser-io/charts/purser \
  --version 0.3.0 -n purser --create-namespace

KEY=$(kubectl -n purser get secret purser-auth \
  -o jsonpath='{.data.api-key}' | base64 -d)

Defaults to the ghcr.io/purser-io/purser images. The chart generates and retains an API-key Secret.

# run the REST API
docker run --rm -p 8080:8080 ghcr.io/purser-io/purser:0.3.0

# …or scan a directory with the CLI
docker run --rm -v "$PWD:/models" ghcr.io/purser-io/purser:0.3.0 \
  purser scan /models --policy /models/policy.yaml

Multi-arch (amd64 + arm64), Wolfi-based, cosign-signed. Also -hf (Hub scans + upstream verdict ingestion) and -deep (deep-analysis companion) variants.

pip install "purser[sign]"          # from PyPI

purser scan ./model.safetensors --policy policies/default.yaml

# …or from source:
git clone https://github.com/purser-io/purser && cd purser
uv pip install -e ".[sign]"

Python 3.11+. Installable from PyPI or source. Pre-1.0 — APIs may still change.

Bring a purser aboard.

A purser is the ship's officer who keeps the manifest, verifies it, and clears cargo to board — the clearance desk for every model entering your environment: signals, provenance, and policy behind one enforced verdict.