Modellscan
Eine Modelldatei kann beim Laden Code ausführen. Ein PyTorch-Checkpoint ist ein Python-
Pickle, und beim Entpicklen kann beliebiger Code importiert und aufgerufen werden. Keras-Lambda-Layer,
GGUF-Chat-Templates und benutzerdefinierte ONNX-Operatoren sind weitere Einfallstore. fremforge scannt
Modelldateien überall dort, wo sie in deiner Organisation landen, und führt ein Urteil pro
Datei. Die Richtlinie deiner Organisation entscheidet, was mit einer Datei geschieht, die der Scan findet.
Kein Modell wird je geladen oder ausgeführt, um es zu scannen. Der Scanner liest die Struktur der Datei: die Importliste des Pickles, die Header von safetensors und GGUF, den ONNX- Graphen und die Keras-Konfiguration.
Wo Modelldateien gefunden werden
| Oberfläche | Wann gescannt wird | Was Durchsetzen bewirkt |
|---|---|---|
| Git LFS | Beim Upload | Weist den Upload mit 422 ab |
| Pakete (generisch und jeder Typ, dessen Upload-Body die Datei ist) | Beim Upload | Weist den Upload mit 422 ab |
Container-Registry (ModelPack, KitOps, Docker ai/ und Modelldateien in gewöhnlichen Images) | Nach dem Push | Weist einen Pull, der den Layer ausliefern würde, mit 403 ab |
| Dependency-Proxy (Container-Images) | Beim ersten Pull | Weist spätere Pulls des Layers mit 403 ab |
| Actions-Artefakte | Nach Abschluss des Laufs | Nichts. Artefakte erhalten nur Befunde |
Eine Datei wird an ihren ersten Bytes als Modell erkannt, nicht an ihrem Namen. Ein Pickle,
der als weights.txt gespeichert ist, wird trotzdem gefunden.
Formate und Urteile
| Format | Was geprüft wird |
|---|---|
Pickle, joblib, PyTorch (.pt, .pth, .bin, Legacy und Zip), NumPy-Objekt-Arrays | Jeder Import im Pickle gegen eine Allowlist. Die Allowlist ist das, was torch.load(weights_only=True) von PyTorch akzeptiert, plus die NumPy-Typen, die ein State Dict braucht |
| safetensors | Der Header: Längen, Datentypen und Offsets, und dass sich die Datei nicht zugleich als Zip oder Pickle öffnen lässt |
| GGUF | Die Metadaten, das Chat-Template (gegen eine sichere Teilmenge von Jinja) und dieselben Zip- und Pickle-Prüfungen |
| ONNX | Domänen benutzerdefinierter Operatoren und Pfade für externe Daten, die das Verzeichnis des Modells verlassen |
Keras (.keras, .h5) | Lambda-Layer und benutzerdefinierte Objekte außerhalb des Keras-Namensraums |
Jede Datei erhält ein Urteil, das alle Organisationen teilen, die dieselben Bytes vorhalten:
- Sauber (
clean): nichts gefunden. - Verdächtig (
suspicious): ein Import außerhalb der Allowlist (pickle-unknown-import), eine Klasse aus einem bekannten ML-Framework (pickle-framework-object, siehe Framework-Pickles), ein GGUF-Template-Konstrukt außerhalb der sicheren Teilmenge, ein benutzerdefinierter ONNX-Operator oder eine Datei, die sich auch als anderes Format öffnen lässt. - Schädlich (
malicious): ein Import auf der Blocklist des Scanners (etwaos.systemoderbuiltins.eval) oder ein Keras-Lambda-Layer. - Nicht prüfbar (
unscannable): Die Datei konnte nicht bis zum Ende gelesen werden: ein defektes Zip, ein 7z- Archiv, ein abgeschnittener Stream. - Formatgeprüft (
format-verified): eine safetensors- oder GGUF-Datei über 4 GiB, deren Strukturprüfung bestanden wurde. Siehe Über 4 GiB.
Der Scanner ist Open Source: picklescan für das Durchlaufen des Pickles, ergänzt um die Allowlist von fremforge.
Die Richtlinie deiner Organisation
Die Richtlinie hat drei Modi. Stelle sie unter Sicherheit › Richtlinien › Modellscan ein oder über die API.
| Modus | Was passiert |
|---|---|
| Schatten (Standard) | Jede Datei wird gescannt, und die Dateien, die die Richtlinie blockieren würde, werden als Befunde aufgeführt. Nichts wird abgewiesen. |
| Durchsetzen | Wie Schatten, und die Dateien mit einem offenen Befund werden abgewiesen, wie unter Wo Modelldateien gefunden werden aufgeführt. |
| Aus | Dateien werden weiterhin gescannt und die Befunde weiterhin aufgeführt, aber nichts wird abgewiesen oder als „hätte blockiert" gezählt. |
Unter Durchsetzen blockiert die Richtlinie eine Datei mit dem Urteil Schädlich, Nicht prüfbar oder Verdächtig, sofern der Befund nicht verworfen ist. Eine Datei mit Sauber oder Formatgeprüft besteht immer.
Wie Durchsetzen aussieht
Ein abgewiesener Upload antwortet mit 422, dem Urteil, den Befundcodes und
dem sha256 der Datei:
{
"error": "model file rejected",
"rule": "suspicious",
"verdict": "suspicious",
"findings": ["pickle-unknown-import"],
"sha256": "d24ec017bdcf45a8737b4a9398e6eea64fa37a5821f473041cb3bc03f8489687"
}Eine Datei, die noch nicht gescannt wurde, wird während des Uploads gescannt. Das verlängert ihn bei einer Datei bis 256 MiB um höchstens wenige Sekunden. Eine safetensors- oder GGUF-Datei über 256 MiB wird anhand ihres Headers und ihrer letzten 64 KiB geprüft. Jede andere Modell- datei über 256 MiB wird angenommen und direkt nach dem Upload gescannt, sodass sie eine Minute später als Befund erscheint, statt abgewiesen zu werden.
Ist der Scanner nicht erreichbar, antwortet der Upload einer Modelldatei in einer durchsetzenden
Organisation mit 503 und Retry-After: 30. Uploads anderer Dateien sind
nicht betroffen.
Ein Container-Pull, der einen blockierten Layer ausliefern würde, antwortet mit 403 DENIED, mit
dem Grund model-malicious, model-suspicious oder model-unscannable und dem
Digest des Layers. Ein Modell-Layer, den du gerade gepusht hast, wird innerhalb von etwa einer
Minute gescannt. Bis dahin antwortet ein Pull mit 403 und dem Grund model-scan-pending,
so wie bei einem Image mit einem Layer, dessen Malware-Scan noch läuft. Pulls
durch von fremforge gehostete Runner werden für einen ausstehenden Scan nicht zurückgehalten.
Uploading failed due to unsupported Content-Type header(s) aus, und
git lfs push --object-id endet mit 0. Ein normales git push scheitert dagegen. Der Grund
steht im Response-Body, und die Datei wird als Befund unter
Modellscan aufgeführt.Framework-Pickles
Manche Checkpoints speichern ganze Python-Objekte, nicht nur Gewichte: ein YOLO-Modell, eine
TrainingArguments-Datei, eine scikit-learn-Pipeline. Beim Laden eines solchen läuft der Code dieser
Klasse. Der Scanner kennzeichnet sie mit pickle-framework-object, wenn jeder Import
außerhalb der Gewichte-Allowlist aus einem bekannten Framework stammt: PyTorch-Module,
Transformers, Ultralytics, scikit-learn, joblib, timm, Lightning, SpeechBrain oder
Diffusers.
Sie sind Verdächtig wie jeder andere unbekannte Import. Damit sie unter Durchsetzen bestehen,
schalte Pickles bekannter ML-Frameworks zulassen (allow_framework_pickles) ein. Eine
Datei mit einem anderen Befund wird weiterhin blockiert.
Einen Befund verwerfen
Ein Owner der Organisation kann einen Befund für eine Datei (einen sha256) verwerfen, mit einer Begründung und einem Ablaufdatum: standardmäßig 90 Tage, höchstens 365. Eine verworfene Datei besteht unter Durchsetzen und verschwindet aus der Befundliste. Verwerfungen gelten pro Organisation; eine andere Organisation, die dieselben Bytes vorhält, ist nicht betroffen. Eine Verwerfung lässt sich jederzeit widerrufen.
Über 4 GiB
Der Malware-Scanner ClamAV kann von einer Datei nicht mehr als 4 GiB lesen, und fremforge weist eine Datei ab, die es nicht scannen kann. Für Modellgewichte, die oft größer als 4 GiB sind, gibt es in jedem Modus eine Ausnahme: Eine safetensors- oder GGUF-Datei, deren Strukturprüfung besteht, wird als Formatgeprüft angenommen. Diese Formate enthalten nur Tensoren und Metadaten, und die Strukturprüfung beweist, dass die Datei nichts anderes enthält. Die Datei wird nicht auf Malware gescannt.
Jede andere Datei über 4 GiB, einschließlich jedes Pickles, wird weiterhin abgewiesen. Das Limit für eine einzelne Datei liegt bei 10 GiB. Große Modelle werden in Shards von 5 GB oder weniger veröffentlicht, daher ist das selten das Limit, auf das es ankommt.
Actions-Artefakte
Wenn ein Workflow-Lauf abgeschlossen ist, werden die Dateien, die er mit
actions/upload-artifact hochgeladen hat, auf Modelldateien geprüft, und diese werden gescannt.
Ein Befund wird unter Modellscan mit dem Repository, der Laufnummer
und dem Artefakt aufgeführt. Artefakte werden nie abgewiesen oder gelöscht: Einen
Upload zu blockieren, würde deinen Workflow scheitern lassen, ohne dass du es übersteuern könntest.
KI-Inventar
Sicherheit › Komponenten › KI-Inventar listet die Machine-Learning-Modelle auf, die deine Organisation vorhält, und die, die ihre CI lädt. Es ist schreibgeschützt; Richtlinie und Verwerfungen bleiben auf der Seite Modellscan.
Modelldateien
Eine Zeile pro Modelldatei (pro sha256), gefunden in deinem Git LFS, deinen Paketen, Container- Images, Dependency-Proxy-Pulls und Actions-Artefakten, mit:
- ihrem Format und ihrer Größe sowie ihrem Urteil (Scan ausstehend, solange sie auf ihren Scan wartet);
- ihrer Lizenz, so wie sie deklariert ist (siehe Woher Lizenzen kommen), und wo sie deklariert wurde. Ein Modell ohne deklarierte Lizenz zeigt Unbekannt. fremforge leitet keine Lizenz aus einem Dateinamen ab;
- dem, was ihr Header angibt: Anzahl der Parameter, Datentypen der Tensoren und bei GGUF die Architektur und den Modellnamen;
- wo sie vorkommt: jedes Repository, Paket, Image oder Artefakt, in dem sie gesehen wurde.
Jede Modelldatei steht außerdem im Komponenteninventar
als pkg:generic/<file name>@<sha256>, sodass eine Komponentensuche sie findet und
„Wo wird es verwendet" auch für Modelle funktioniert. Ein Modell, das deine CI von Hugging
Face lädt, steht dort als pkg:huggingface/<org>/<name>@<revision>, beim Repository,
dessen Workflow es lädt.
Woher Lizenzen kommen
fremforge liest eine Lizenz nur aus Metadaten, die bereits bei fremforge gespeichert sind, und fragt niemanden sonst: Nichts über deine Modelle verlässt die Plattform. In dieser Reihenfolge:
- Die Datei selbst: GGUF
general.licenseoder ein Lizenzschlüssel in den Metadaten einer safetensors-Datei. - Das Modellartefakt, dessen Layer die Datei ist, wenn du es in die Registry deiner
Organisation pushst oder über den
Dependency-Proxy lädst. fremforge liest die
kleinen Metadaten-Blobs des Artefakts aus seinem eigenen Speicher, nie die Gewichte und
nie die Upstream-Registry:
Verpackung Gelesen ModelPack ( modctl)descriptor.licensesder ConfigKitOps ( kit)das Kitfile: die licenseeines Modellteils für diesen Teil, dannmodel.license, dannpackage.licenseDocker Model Runner ( docker model,ai/…)GGUF general.licensein der Modell-Config, sonst die Lizenzdatei im Artefakt (application/vnd.docker.ai.license): Apache-2.0 und MIT werden an ihrem Text erkannt, jede andere Lizenz an ihrem Titel, etwaLLAMA 3.2 COMMUNITY LICENSE AGREEMENTjede davon die Manifest-Annotation org.opencontainers.image.licenses
Die API liefert, wo jede Lizenz deklariert wurde, als licence_from, zum Beispiel
ModelPack config descriptor.licenses; das KI-Inventar zeigt es neben der
Lizenz. Eine Lizenz aus einem Image, das du gepusht hast, wird nur deiner
Organisation angezeigt. Ein Modell, das vor dem 13. Oktober 2026 geladen wurde, erhält seine Lizenz aus dem
Artefakt innerhalb einer Stunde, sofern das Artefakt noch gespeichert ist.
Modelle, die deine CI lädt
Der tägliche Workflow-Scan liest jeden Workflow auf dem Standard-Branch jedes Repositorys
sowie die Python- und Shell-Skripte, die ein Workflow aus demselben
Repository ausführt (python scripts/fetch.py, bash ci/prepare.sh). Er findet:
| Werkzeug | Gefunden als |
|---|---|
| Hugging Face | from_pretrained("org/name"), hf_hub_download(repo_id=…), snapshot_download(…), huggingface-cli download, hf download |
| Ollama | ollama pull name:tag |
| Docker | docker model pull ref |
| KitOps | kit pull ref |
| ModelPack | modctl pull ref |
Eine Referenz ist festgelegt, wenn sie sich unter deinem Build nicht ändern kann: ein Hugging-
Face-Modell mit einem vollständigen 40-stelligen Commit-SHA als revision
(revision="<sha>" oder --revision <sha>) und ein Ollama-, Docker-, KitOps- oder
ModelPack-Modell mit einem @sha256:-Digest. Ein Branch oder Tag wie main oder
v1.0 ist nicht festgelegt. Jede nicht festgelegte Referenz ist ein Befund, wie eine Action,
die auf einen Tag statt auf einen Commit festgelegt ist.
Ein Modellname, der nicht in der Datei steht, etwa
from_pretrained(model_name), wird als Name zur Laufzeit gesetzt aufgeführt. Ein
HF_ENDPOINT, das in derselben Datei gesetzt ist, wird angezeigt, weil der Pull dann an diesen
Endpunkt statt an huggingface.co geht. Eine Referenz, die du entfernst, verschwindet innerhalb von
drei Tagen.
Nicht gefunden: oras pull (ein ORAS-Artefakt ist nur an seinem Medientyp als Modell erkennbar,
den der Befehl nicht zeigt; über den Dependency-Proxy geladen, wird es
an seinem Manifest eingeordnet) und Python-Code außerhalb der Skripte, die ein Workflow
ausführt.
Die Lizenz eines Hugging-Face-Modells, das deine CI per Namen lädt, wird als Unbekannt angezeigt: fremforge kontaktiert weder Hugging Face noch sonst jemanden, um sie nachzuschlagen. Ein Modell, das deine CI als OCI-Artefakt über den Dependency-Proxy lädt, erhält seine Lizenz aus dem Artefakt (siehe Woher Lizenzen kommen).
ML-BOM
ML-BOM herunterladen (CycloneDX) auf der Seite oder GET /ai/ml-bom liefert das
Inventar als CycloneDX-1.6-Dokument. Jede Modelldatei ist eine Komponente vom Typ
machine-learning-model mit ihrem sha256, ihrer deklarierten Lizenz (wo sie
deklariert wurde, steht in der Eigenschaft fremforge:model:licence_from), ihrer
Architektur als modelCard.modelParameters.modelArchitecture, wenn der Header
eine nennt, sowie ihrem Format, Urteil, ihren Oberflächen, ihrer Größe, Parameteranzahl und ihren Daten-
typen als Eigenschaften fremforge:model:*. Es werden nur Fakten angegeben, die aus der Datei
gelesen wurden.
Die SBOMs, die deine CI hochlädt, werden nicht verändert: Es sind deine Dateien. Die ML-BOM ist ein eigenes Dokument.
Compliance-Bericht
Der Compliance-Bericht hat einen Abschnitt KI-Inventar: die Anzahl der Modelldateien, die Dateien, die deine Richtlinie blockiert oder blockieren würde, die Dateien mit einem verworfenen Befund, deinen Modellscan-Modus, die Modell-Pulls der CI ohne feste Revision und die Dateien pro deklarierter Lizenz. Seine CSV listet jede Modelldatei auf. Der Abschnitt deckt immer die ganze Organisation ab, auch in einem Bericht für ein einzelnes Repository.
Audit-Log
| Ereignis | Wann |
|---|---|
security_policy.model_scan.updated | Der Modus oder die Framework-Pickle-Einstellung wurde geändert |
security_policy.model_scan.dismissed | Ein Befund wurde verworfen oder eine Verwerfung verlängert |
security_policy.model_scan.dismissal_revoked | Eine Verwerfung wurde widerrufen |
registry.pull.blocked mit gate: model | Ein Pull wurde von der Modellrichtlinie abgewiesen |
Ein abgewiesener Paket-Upload oder Container-Pull steht außerdem im
Zugriffsprotokoll der Registry, mit dem Grund model_policy:<rule>.
Das Zugriffsprotokoll deckt Git LFS nicht ab.
API
| Aufruf | Scope |
|---|---|
GET /_app/api/v1/orgs/{org}/security/model-policy | policy:read |
PUT /_app/api/v1/orgs/{org}/security/model-policy | policy:write |
GET /_app/api/v1/orgs/{org}/security/model-findings?status=open|dismissed|all | findings:read |
POST /_app/api/v1/orgs/{org}/security/model-findings/{sha256}/dismiss | policy:write |
DELETE /_app/api/v1/orgs/{org}/security/model-findings/{sha256}/dismissal | policy:write |
GET /_app/api/v1/orgs/{org}/ai/models?verdict=&format=&licence=&surface= | components:read |
GET /_app/api/v1/orgs/{org}/ai/models/{sha256} | components:read |
GET /_app/api/v1/orgs/{org}/ai/ml-bom | components:read |
GET /_app/api/v1/orgs/{org}/ai/references?tool=&unpinned=true&repository=&model= | components:read |
PUT ersetzt die gesamte Richtlinie: Ein Feld, das du weglässt, erhält seinen Standardwert.
curl -X PUT \
-H "Authorization: Bearer ${FREMFORGE_PAT}" \
-H "Content-Type: application/json" \
-d '{"mode":"enforce","allow_framework_pickles":false}' \
https://frem.sh/_app/api/v1/orgs/acme/security/model-policyZum Verwerfen braucht es policy:write, nicht findings:write, weil eine verworfene Datei
unter Durchsetzen besteht. Das Runner-Token eines CI-Jobs trägt diesen Scope nie.
curl -X POST \
-H "Authorization: Bearer ${FREMFORGE_PAT}" \
-H "Content-Type: application/json" \
-d '{"reason":"Our own YOLO checkpoint, built in CI","expires_at":"2027-01-31"}' \
https://frem.sh/_app/api/v1/orgs/acme/security/model-findings/<sha256>/dismissDie Struktur der Antworten steht in der API-Referenz.
Was nicht abgedeckt ist
- Dateien, die an Issues, Pull Requests und Releases angehängt sind, sowie Paket-Uploads, deren Body mehr als die Datei enthält (PyPI, NuGet und andere Multipart- Uploads). Diese werden wie bisher auf Malware gescannt.
- Modelle in den gewöhnlichen Git-Objekten deiner Repositorys. Große Modelldateien gehören in Git LFS.
- Ein Modell laden oder ausführen und eine Hintertür in seinen Gewichten finden. Kein Scanner kann ein vergiftetes Gewicht von einem normalen unterscheiden.