v0.1.0 released — signed images & Helm chart

Clear every model
before it comes aboard.

Purser statically scans machine-learning model artifacts for malicious code and data-exfiltration indicators — and enforces your policy: restrict by country of origin, publisher, name, or format. Nothing is ever deserialized or executed.

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

17+model formats
0bytes ever executed
Apache-2.0open source
Ed25519signed 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 as a pre-load check in Kubernetes. Set policy once (severity, formats, publishers, country of origin) and let the exit code fail the build.

Set up scanning →

ML engineers & data scientists

Vet a model you just pulled from Hugging Face before you load it. One command tells you whether it hides code, phones home, or breaks your org's rules — without ever executing it.

Scan a model →

Detection

Best-of-breed scanning across the model zoo

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.

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, 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 (+ gzip) and UTF-16 payloads — decoded and re-analyzed.

Also: ExecuTorch · TF.js · CoreML · skops · PaddlePaddle · PMML · NumPy · archives (zip-slip/bomb) · HF config.json auto_map.

Why Purser

A scanner, a policy engine, and a supply-chain story

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. 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.

Signed supply chain

Hash-pinned lockfiles, CycloneDX SBOMs, THIRD_PARTY_LICENSES, and cosign-keyless-signed images and chart — with a CI vulnerability gate.

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.1.0
  script:
    - purser scan ./models \
        --policy .purser/policy.yaml \
        --format sarif --output purser.sarif

Kubernetes pre-load gate

Deploy once; a serving controller or registry webhook calls the API and only mounts a model whose verdict is PASS/WARN.

curl -s -H "X-API-Key: $KEY" \
  -F "file=@model.safetensors" \
  http://purser.purser.svc/v1/scan/upload \
  | jq .verdict

How Purser compares

Where Purser sits among ML model scanners

Best-effort assessment of publicly documented features. Legend: ✅ yes · ◐ partial · ❌ no.

CapabilityPurserpicklescanFicklingModelScanModelAudit
Pickle opcode malware scan
Broad format coverage
Data-exfiltration engine
trust_remote_code AST
Policy: country / publisher / name
Signed provenance / trust store
REST API + K8s + Helm
Prometheus / audit observability
LicenseApache-2.0OSSOSSApache-2.0MIT

ModelAudit leads on raw format-scanner depth (TensorRT/OpenVINO) and maturity; commercial tools add CVE/threat-intel feeds and behavioral backdoor detection Purser does not attempt.

Install

Up and running in a minute

helm install purser oci://ghcr.io/purser-io/charts/purser \
  --version 0.1.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.1.0

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

Multi-arch (amd64 + arm64), Wolfi-based, cosign-signed. Also -hf and -deep variants.

git clone https://github.com/purser-io/purser
cd purser
uv pip install -e ".[sign]"

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

Python 3.11+. Pre-1.0 and not yet on PyPI — install from source or use the container images above.

Bring a purser aboard.

A purser is the ship's officer who keeps the manifest, verifies it, and clears cargo to board — scan, provenance, and policy in one open-source tool.