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 →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.
$ purser scan ./models --policy policy.yaml
✔ resnet50.safetensors PASS Ed25519 verified — acme-ml · no code
▲ tokenizer.pkl WARN pickle REDUCE — review advised
✗ modeling_custom.py FAIL os.system in trust_remote_code
■ vendor-drop.bin BLOCKED origin: CN not in allowlist
4 artifacts • 1 pass • 1 warn • 1 fail • 1 blocked
exit code 2 → pipeline gated
Who it's for
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 →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
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.
.pkl .pt .pth .bin .ckpt .joblib — dangerous imports via GLOBAL/STACK_GLOBAL, multi-pickle streams, REDUCE-on-load, evasive opcodes.
Lambda & TFOpLambda bytecode layers, non-builtin custom layers (external code on load), PyFunc/EagerPyFunc graph ops, TFLite Flex delegates, file-access kernels.
Custom Python operator domains, external-data path traversal, and safetensors header validation against parser-bug exploits.
Chat-template Jinja SSTI injection — __subclasses__, os. access, and dynamic code smuggled in templates.
Full AST analysis of trust_remote_code: exec/eval, sockets & HTTP, dynamic import, marshal, and base64/hex deobfuscation.
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.
lit = dedicated scanner · dim = format-ID for policy + full exfiltration scan
Why Purser
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.
YAML policy with severity thresholds and allow/blocklists for formats, publishers, model-name globs, and country of origin. Per-rule overrides; require_signed.
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.
Ships as a CLI, a REST API, digest-pinned Wolfi container images, plain kustomize manifests, and a production Helm chart with hardened defaults.
Prometheus /metrics (verdicts, categories, formats, origins, latencies), a structured syslog/JSON audit log, and a Grafana dashboard (pictured below).
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.
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.
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
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.
In your pipeline
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
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
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.
| Capability | Purser | picklescan | Fickling | ModelScan | ModelAudit |
|---|---|---|---|---|---|
| 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 | ✅ | ❌ | ❌ | ❌ | ❌ |
| License | Apache-2.0 | OSS | OSS | Apache-2.0 | MIT |
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
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.
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.