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 →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.
$ purser scan ./models --policy policy.yaml
✔ resnet50.safetensors PASS header validated, 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 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 →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
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.
.pkl .pt .pth .bin .ckpt .joblib — dangerous imports via GLOBAL/STACK_GLOBAL, multi-pickle streams, REDUCE-on-load, evasive opcodes.
Lambda & TFOpLambda bytecode layers, 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 (+ 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
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. 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.
Hash-pinned lockfiles, CycloneDX SBOMs, THIRD_PARTY_LICENSES, and cosign-keyless-signed images and chart — with a CI vulnerability gate.
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.1.0
script:
- purser scan ./models \
--policy .purser/policy.yaml \
--format sarif --output purser.sarif
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
Best-effort assessment of publicly documented features. Legend: ✅ yes · ◐ partial · ❌ no.
| Capability | Purser | picklescan | Fickling | ModelScan | ModelAudit |
|---|---|---|---|---|---|
| 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 | ✅ | ❌ | ❌ | ❌ | ❌ |
| License | Apache-2.0 | OSS | OSS | Apache-2.0 | MIT |
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
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.
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.