Zum Hauptinhalt springen

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ächeWann gescannt wirdWas Durchsetzen bewirkt
Git LFSBeim UploadWeist den Upload mit 422 ab
Pakete (generisch und jeder Typ, dessen Upload-Body die Datei ist)Beim UploadWeist den Upload mit 422 ab
Container-Registry (ModelPack, KitOps, Docker ai/ und Modelldateien in gewöhnlichen Images)Nach dem PushWeist einen Pull, der den Layer ausliefern würde, mit 403 ab
Dependency-Proxy (Container-Images)Beim ersten PullWeist spätere Pulls des Layers mit 403 ab
Actions-ArtefakteNach Abschluss des LaufsNichts. 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

FormatWas geprüft wird
Pickle, joblib, PyTorch (.pt, .pth, .bin, Legacy und Zip), NumPy-Objekt-ArraysJeder 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
safetensorsDer Header: Längen, Datentypen und Offsets, und dass sich die Datei nicht zugleich als Zip oder Pickle öffnen lässt
GGUFDie Metadaten, das Chat-Template (gegen eine sichere Teilmenge von Jinja) und dieselben Zip- und Pickle-Prüfungen
ONNXDomä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 (etwa os.system oder builtins.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.

ModusWas passiert
Schatten (Standard)Jede Datei wird gescannt, und die Dateien, die die Richtlinie blockieren würde, werden als Befunde aufgeführt. Nichts wird abgewiesen.
DurchsetzenWie Schatten, und die Dateien mit einem offenen Befund werden abgewiesen, wie unter Wo Modelldateien gefunden werden aufgeführt.
AusDateien 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.

git-lfs verbirgt den Grund. Bei einem abgewiesenen Upload gibt git-lfs 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:

  1. Die Datei selbst: GGUF general.license oder ein Lizenzschlüssel in den Metadaten einer safetensors-Datei.
  2. 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:
    VerpackungGelesen
    ModelPack (modctl)descriptor.licenses der Config
    KitOps (kit)das Kitfile: die license eines Modellteils für diesen Teil, dann model.license, dann package.license
    Docker Model Runner (docker model, ai/…)GGUF general.license in 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, etwa LLAMA 3.2 COMMUNITY LICENSE AGREEMENT
    jede davondie 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:

WerkzeugGefunden als
Hugging Facefrom_pretrained("org/name"), hf_hub_download(repo_id=…), snapshot_download(…), huggingface-cli download, hf download
Ollamaollama pull name:tag
Dockerdocker model pull ref
KitOpskit pull ref
ModelPackmodctl 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

EreignisWann
security_policy.model_scan.updatedDer Modus oder die Framework-Pickle-Einstellung wurde geändert
security_policy.model_scan.dismissedEin Befund wurde verworfen oder eine Verwerfung verlängert
security_policy.model_scan.dismissal_revokedEine Verwerfung wurde widerrufen
registry.pull.blocked mit gate: modelEin 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

AufrufScope
GET /_app/api/v1/orgs/{org}/security/model-policypolicy:read
PUT /_app/api/v1/orgs/{org}/security/model-policypolicy:write
GET /_app/api/v1/orgs/{org}/security/model-findings?status=open|dismissed|allfindings:read
POST /_app/api/v1/orgs/{org}/security/model-findings/{sha256}/dismisspolicy:write
DELETE /_app/api/v1/orgs/{org}/security/model-findings/{sha256}/dismissalpolicy: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-bomcomponents: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-policy

Zum 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>/dismiss

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