Modelscan
Een modelbestand kan code uitvoeren wanneer het wordt geladen. Een PyTorch-checkpoint
is een Python-pickle, en bij het unpicklen kan alles worden geïmporteerd en aangeroepen.
Keras-Lambda-lagen, GGUF-chattemplates en aangepaste ONNX-operators zijn andere
ingangen. fremforge scant modelbestanden overal waar ze in je organisatie terechtkomen
en houdt per bestand één oordeel bij. Het beleid van je organisatie bepaalt wat er
gebeurt met een bestand waarin de scan iets vindt.
Er wordt nooit een model geladen of uitgevoerd om het te scannen. De scanner leest de structuur van het bestand: de importlijst van de pickle, de headers van safetensors en GGUF, de ONNX-graaf en de Keras-configuratie.
Waar modelbestanden worden gevonden
| Plek | Wanneer gescand | Wat afdwingen doet |
|---|---|---|
| Git LFS | Bij het uploaden | Weigert de upload met 422 |
| Pakketten (generiek, en elk type waarvan de uploadbody het bestand zelf is) | Bij het uploaden | Weigert de upload met 422 |
Containerregistry (ModelPack, KitOps, Docker ai/ en modelbestanden in gewone images) | Na de push | Weigert een pull die de laag zou leveren met 403 |
| Proxy voor afhankelijkheden (container-images) | Bij de eerste pull | Weigert latere pulls van de laag met 403 |
| Actions-artefacten | Nadat de run klaar is | Niets. Artefacten krijgen alleen bevindingen |
Een bestand wordt herkend als model aan de eerste bytes, niet aan de naam. Een pickle
die als weights.txt is opgeslagen, wordt toch gevonden.
Formaten en oordelen
| Formaat | Wat wordt gecontroleerd |
|---|---|
Pickle, joblib, PyTorch (.pt, .pth, .bin, legacy en zip), NumPy-objectarrays | Elke import in de pickle tegen een allowlist. De allowlist is wat PyTorch’ torch.load(weights_only=True) accepteert, plus de NumPy-typen die een state dict nodig heeft |
| safetensors | De header: lengtes, datatypen en offsets, en dat het bestand niet ook als zip of pickle te openen is |
| GGUF | De metadata, de chattemplate (tegen een veilige subset van Jinja), en dezelfde zip- en picklecontroles |
| ONNX | Domeinen van aangepaste operators, en paden naar externe data die buiten de map van het model vallen |
Keras (.keras, .h5) | Lambda-lagen en aangepaste objecten buiten de Keras-namespace |
Elk bestand krijgt één oordeel, gedeeld door elke organisatie die dezelfde bytes bewaart:
- clean: niets gevonden.
- suspicious: een import buiten de allowlist (
pickle-unknown-import), een klasse uit een bekend ML-framework (pickle-framework-object, zie Framework-pickles), een GGUF-templateconstructie buiten de veilige subset, een aangepaste ONNX-operator, of een bestand dat ook als ander formaat te openen is. - malicious: een import op de blocklist van de scanner (zoals
os.systemofbuiltins.eval), of een Keras-Lambda-laag. - unscannable: het bestand kon niet tot het einde worden gelezen: een kapotte zip, een 7z-archief, een afgebroken stream.
- format-verified: een safetensors- of GGUF-bestand boven 4 GiB waarvan de structuurcontrole is geslaagd. Zie Boven 4 GiB.
De scanner is open source: picklescan voor het doorlopen van de pickle, met de allowlist van fremforge daarbovenop.
Het beleid van je organisatie
Het beleid kent drie modi. Stel het in onder Beveiliging › Beleid › Modelscan, of met de API.
| Modus | Wat er gebeurt |
|---|---|
| Schaduw (standaard) | Elk bestand wordt gescand en de bestanden die het beleid zou blokkeren, staan als bevindingen in de lijst. Er wordt niets geweigerd. |
| Afdwingen | Als schaduw, en bestanden met een open bevinding worden geweigerd, zoals beschreven onder Waar modelbestanden worden gevonden. |
| Uit | Bestanden worden nog steeds gescand en de bevindingen staan nog in de lijst, maar er wordt niets geweigerd of als ‘zou blokkeren’ geteld. |
Bij afdwingen blokkeert het beleid een bestand met het oordeel malicious, unscannable of suspicious, tenzij de bevinding is genegeerd. Een bestand met clean of format-verified komt altijd door.
Hoe afdwingen eruitziet
Een geweigerde upload antwoordt met 422, het oordeel, de bevindingscodes en
de sha256 van het bestand:
{
"error": "model file rejected",
"rule": "suspicious",
"verdict": "suspicious",
"findings": ["pickle-unknown-import"],
"sha256": "d24ec017bdcf45a8737b4a9398e6eea64fa37a5821f473041cb3bc03f8489687"
}Een bestand dat nog niet eerder is gescand, wordt tijdens de upload gescand. Dat kost hooguit een paar seconden voor een bestand tot 256 MiB. Een safetensors- of GGUF-bestand boven 256 MiB wordt gecontroleerd aan de hand van de header en de laatste 64 KiB. Elk ander modelbestand boven 256 MiB wordt geaccepteerd en direct na de upload gescand, zodat het een minuut later als bevinding verschijnt in plaats van geweigerd te worden.
Als de scanner niet bereikbaar is, antwoordt een upload van een modelbestand in een
organisatie die afdwingt met 503 en Retry-After: 30. Uploads van andere bestanden
merken daar niets van.
Een containerpull die een geblokkeerde laag zou leveren, antwoordt met 403 DENIED,
met de reden model-malicious, model-suspicious of model-unscannable en de
digest van de laag. Een modellaag die je net hebt gepusht, wordt binnen ongeveer een
minuut gescand. Tot dan antwoordt een pull met 403 en de reden model-scan-pending,
net als bij een image met een laag die nog op malware wordt gescand. Pulls door
runners die fremforge host, worden niet vastgehouden voor een lopende scan.
Uploading failed due to unsupported Content-Type header(s), en
git lfs push --object-id eindigt met 0. Een gewone git push mislukt wel. De reden
staat in de responsebody, en het bestand staat als bevinding onder
Modelscan.Framework-pickles
Sommige checkpoints slaan complete Python-objecten op, niet alleen gewichten: een
YOLO-model, een TrainingArguments-bestand, een scikit-learn-pipeline. Bij het laden
draait de code van die klasse. De scanner labelt deze als pickle-framework-object
wanneer elke import buiten de allowlist voor gewichten uit een bekend framework komt:
PyTorch-modules, Transformers, Ultralytics, scikit-learn, joblib, timm, Lightning,
SpeechBrain of Diffusers.
Ze zijn suspicious, net als elke andere onbekende import. Wil je ze door afdwingen
laten komen, zet dan Pickles van bekende ML-frameworks toestaan aan
(allow_framework_pickles). Een bestand met een andere bevinding blijft geblokkeerd.
Een bevinding negeren
Een eigenaar van de organisatie kan een bevinding negeren voor één bestand (één sha256), met een reden en een vervaldatum: standaard 90 dagen, hooguit 365. Een genegeerd bestand komt door afdwingen en verdwijnt uit de lijst met bevindingen. Negeren geldt per organisatie; een andere organisatie die dezelfde bytes bewaart, merkt er niets van. Je kunt het negeren op elk moment intrekken.
Boven 4 GiB
De malwarescanner, ClamAV, kan niet meer dan 4 GiB van één bestand lezen, en fremforge weigert een bestand dat het niet kan scannen. Voor modelgewichten, die vaak groter zijn dan 4 GiB, geldt in elke modus één uitzondering: een safetensors- of GGUF-bestand waarvan de structuurcontrole slaagt, wordt geaccepteerd als format-verified. Deze formaten bevatten alleen tensors en metadata, en de structuurcontrole bewijst dat het bestand niets anders bevat. Het bestand wordt niet op malware gescand.
Elk ander bestand boven 4 GiB, waaronder elke pickle, wordt nog steeds geweigerd. De limiet voor één bestand is 10 GiB. Grote modellen worden gepubliceerd in shards van 5 GB of kleiner, dus dit is zelden de limiet die ertoe doet.
Actions-artefacten
Wanneer een workflowrun klaar is, worden de bestanden die hij met
actions/upload-artifact heeft geüpload, gecontroleerd op modelbestanden, en die
worden gescand. Een bevinding staat onder Modelscan met de repository, het
runnummer en het artefact. Artefacten worden nooit geweigerd of verwijderd: een upload
blokkeren zou je workflow laten mislukken zonder dat je dat kunt overrulen.
AI-inventaris
Beveiliging › Componenten › AI-inventaris toont de machine-learningmodellen die je organisatie bewaart en de modellen die haar CI ophaalt. De pagina is alleen-lezen; het beleid en het negeren van bevindingen blijven op de pagina van de modelscan.
Modelbestanden
Eén rij per modelbestand (per sha256) dat is gevonden in je Git LFS, packages, container-images, pulls via de dependency proxy en Actions-artefacten, met:
- het formaat en de grootte, en het oordeel (Wacht op scan zolang de scan nog moet gebeuren);
- de licentie, zoals die is opgegeven (zie Waar licenties vandaan komen), met waar die is opgegeven. Een model zonder opgegeven licentie toont Onbekend. fremforge leidt geen licentie af uit een bestandsnaam;
- wat de header vermeldt: het aantal parameters, de datatypen van de tensors, en voor GGUF de architectuur en de modelnaam;
- waar het voorkomt: elke repository, package, image of elk artefact waarin het is gezien.
Elk modelbestand staat ook in de componenteninventaris
als pkg:generic/<file name>@<sha256>, zodat een zoekopdracht naar componenten het
vindt en ‘waar wordt het gebruikt’ ook voor modellen werkt. Een model dat je CI van
Hugging Face ophaalt, staat er als pkg:huggingface/<org>/<name>@<revision>, bij de
repository waarvan de workflow het ophaalt.
Waar licenties vandaan komen
fremforge leest een licentie alleen uit metadata die al op fremforge is opgeslagen, en vraagt het aan niemand anders: niets over je modellen verlaat het platform. In deze volgorde:
- Het bestand zelf: GGUF
general.license, of een licentiesleutel in de metadata van een safetensors-bestand. - Het modelartefact waarvan het bestand een laag is, wanneer je het naar de
registry van je organisatie pusht of via de
dependency proxy ophaalt. fremforge leest de
kleine metadatablobs van het artefact uit zijn eigen opslag, nooit de gewichten en
nooit de upstream-registry:
Verpakking Gelezen ModelPack ( modctl)descriptor.licensesin de configKitOps ( kit)de Kitfile: de licensevan een modelonderdeel voor dat onderdeel, danmodel.license, danpackage.licenseDocker Model Runner ( docker model,ai/…)GGUF general.licensein de modelconfig, anders het licentiebestand in het artefact (application/vnd.docker.ai.license): Apache-2.0 en MIT worden herkend aan hun tekst, elke andere licentie aan haar titel, zoalsLLAMA 3.2 COMMUNITY LICENSE AGREEMENTelk van deze de manifestannotatie org.opencontainers.image.licenses
De API geeft als licence_from terug waar elke licentie is opgegeven, bijvoorbeeld
ModelPack config descriptor.licenses; de AI-inventaris toont het naast de licentie.
Een licentie uit een image dat je zelf hebt gepusht, is alleen zichtbaar voor je
organisatie. Een model dat vóór 13 oktober 2026 is opgehaald, krijgt binnen een uur
zijn licentie uit het artefact, als het artefact nog is opgeslagen.
Modellen die je CI ophaalt
De dagelijkse workflowscan leest elke workflow op de standaardbranch van elke
repository, en de Python- en shellscripts die een workflow uit dezelfde repository
draait (python scripts/fetch.py, bash ci/prepare.sh). Hij vindt:
| Tool | Gevonden 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 |
Een verwijzing is vastgepind als hij onder je build niet kan veranderen: een
Hugging Face-model met een volledige commit-sha van 40 tekens als revision
(revision="<sha>" of --revision <sha>), en een Ollama-, Docker-, KitOps- of
ModelPack-model met een @sha256:-digest. Een branch of tag zoals main of v1.0
is niet vastgepind. Elke niet-vastgepinde verwijzing is een bevinding, net als een
action die aan een tag in plaats van een commit is vastgepind.
Een modelnaam die niet in het bestand staat, zoals from_pretrained(model_name),
staat in de lijst als Naam bepaald tijdens uitvoering. Een HF_ENDPOINT die in
hetzelfde bestand is ingesteld, wordt getoond, omdat de pull dan naar dat endpoint gaat
in plaats van naar huggingface.co. Een verwijzing die je weghaalt, verdwijnt binnen
drie dagen.
Niet gevonden: oras pull (een ORAS-artefact is alleen aan zijn mediatype als model
te herkennen, en dat toont het commando niet; via de dependency proxy opgehaald wordt
het aan de hand van zijn manifest geclassificeerd), en Python-code buiten de scripts
die een workflow draait.
De licentie van een Hugging Face-model dat je CI op naam ophaalt, wordt getoond als Onbekend: fremforge neemt geen contact op met Hugging Face, of met wie dan ook, om die op te zoeken. Een model dat je CI als OCI-artefact via de dependency proxy ophaalt, krijgt zijn licentie uit het artefact (zie Waar licenties vandaan komen).
ML-BOM
ML-BOM downloaden (CycloneDX) op de pagina, of GET /ai/ml-bom, geeft de
inventaris terug als CycloneDX 1.6-document. Elk modelbestand is een component van het
type machine-learning-model met zijn sha256, zijn opgegeven licentie (waar die is
opgegeven, staat in de eigenschap fremforge:model:licence_from), zijn architectuur
als modelCard.modelParameters.modelArchitecture wanneer de header er een noemt, en
zijn formaat, oordeel, vindplaatsen, grootte, aantal parameters en datatypen als
fremforge:model:*-eigenschappen. Alleen feiten die uit het bestand zijn gelezen,
worden vermeld.
De SBOM’s die je CI uploadt, worden niet gewijzigd: het zijn jouw bestanden. De ML-BOM is een apart document.
Compliancerapport
Het compliancerapport heeft een sectie AI-inventaris: het aantal modelbestanden, de bestanden die je beleid blokkeert of zou blokkeren, de bestanden met een genegeerde bevinding, de modus van je modelscan, de modelpulls van je CI zonder vaste revisie, en het aantal bestanden per opgegeven licentie. De CSV ervan bevat elk modelbestand. De sectie beslaat altijd de hele organisatie, ook in een rapport voor één repository.
Auditlog
| Event | Wanneer |
|---|---|
security_policy.model_scan.updated | De modus of de instelling voor framework-pickles is gewijzigd |
security_policy.model_scan.dismissed | Een bevinding is genegeerd, of het negeren is verlengd |
security_policy.model_scan.dismissal_revoked | Het negeren is ingetrokken |
registry.pull.blocked met gate: model | Een pull is door het modelbeleid geweigerd |
Een geweigerde package-upload of containerpull staat ook in het
toegangslog van de registry, met de reden
model_policy:<rule>. Het toegangslog dekt Git LFS niet.
API
| Aanroep | 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 vervangt het hele beleid: een veld dat je weglaat, krijgt zijn standaardwaarde.
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-policyNegeren vereist policy:write, niet findings:write, omdat een genegeerd bestand door
afdwingen komt. Het runnertoken van een CI-job heeft die scope nooit.
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>/dismissZie de API-referentie voor de vorm van de antwoorden.
Wat niet wordt gedekt
- Bestanden die aan issues, pull requests en releases zijn toegevoegd, en package-uploads waarvan de body meer bevat dan het bestand (PyPI, NuGet en andere multipart-uploads). Die worden zoals voorheen op malware gescand.
- Modellen in de gewone Git-objecten van je repository’s. Grote modelbestanden horen in Git LFS.
- Een model laden of uitvoeren, en een backdoor in de gewichten vinden. Geen scanner kan een vergiftigd gewicht van een normaal gewicht onderscheiden.