Naar de hoofdinhoud

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

PlekWanneer gescandWat afdwingen doet
Git LFSBij het uploadenWeigert de upload met 422
Pakketten (generiek, en elk type waarvan de uploadbody het bestand zelf is)Bij het uploadenWeigert de upload met 422
Containerregistry (ModelPack, KitOps, Docker ai/ en modelbestanden in gewone images)Na de pushWeigert een pull die de laag zou leveren met 403
Proxy voor afhankelijkheden (container-images)Bij de eerste pullWeigert latere pulls van de laag met 403
Actions-artefactenNadat de run klaar isNiets. 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

FormaatWat wordt gecontroleerd
Pickle, joblib, PyTorch (.pt, .pth, .bin, legacy en zip), NumPy-objectarraysElke 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
safetensorsDe header: lengtes, datatypen en offsets, en dat het bestand niet ook als zip of pickle te openen is
GGUFDe metadata, de chattemplate (tegen een veilige subset van Jinja), en dezelfde zip- en picklecontroles
ONNXDomeinen 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.system of builtins.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.

ModusWat 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.
AfdwingenAls schaduw, en bestanden met een open bevinding worden geweigerd, zoals beschreven onder Waar modelbestanden worden gevonden.
UitBestanden 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.

git-lfs verbergt de reden. Bij een geweigerde upload toont git-lfs 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:

  1. Het bestand zelf: GGUF general.license, of een licentiesleutel in de metadata van een safetensors-bestand.
  2. 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:
    VerpakkingGelezen
    ModelPack (modctl)descriptor.licenses in de config
    KitOps (kit)de Kitfile: de license van een modelonderdeel voor dat onderdeel, dan model.license, dan package.license
    Docker Model Runner (docker model, ai/…)GGUF general.license in 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, zoals LLAMA 3.2 COMMUNITY LICENSE AGREEMENT
    elk van dezede 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:

ToolGevonden 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

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

EventWanneer
security_policy.model_scan.updatedDe modus of de instelling voor framework-pickles is gewijzigd
security_policy.model_scan.dismissedEen bevinding is genegeerd, of het negeren is verlengd
security_policy.model_scan.dismissal_revokedHet negeren is ingetrokken
registry.pull.blocked met gate: modelEen 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

AanroepScope
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 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-policy

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

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