Analyse des modèles
Un fichier de modèle peut exécuter du code lorsqu’il est chargé. Un checkpoint
PyTorch est un pickle Python, et le désérialiser peut importer et appeler
n’importe quoi. Les couches Keras Lambda, les modèles de conversation GGUF et
les opérateurs personnalisés ONNX sont d’autres points d’entrée. fremforge
analyse les fichiers de modèles où qu’ils arrivent dans votre organisation et
conserve un verdict par fichier. La politique de votre organisation décide du
sort d’un fichier signalé par l’analyse.
Aucun modèle n’est jamais chargé ni exécuté pour être analysé. L’analyseur lit la structure du fichier : la liste des imports du pickle, les en-têtes safetensors et GGUF, le graphe ONNX et la configuration Keras.
Où les fichiers de modèles sont trouvés
| Surface | Moment de l’analyse | Effet du mode Appliquer |
|---|---|---|
| Git LFS | À l’envoi | Refuse l’envoi avec 422 |
| Paquets (génériques, et tout type dont le corps d’envoi est le fichier) | À l’envoi | Refuse l’envoi avec 422 |
Registre de conteneurs (ModelPack, KitOps, Docker ai/ et fichiers de modèles dans des images ordinaires) | Après le push | Refuse avec 403 un pull qui servirait la couche |
| Proxy de dépendances (images de conteneurs) | Au premier pull | Refuse avec 403 les pulls suivants de la couche |
| Artefacts Actions | À la fin de l’exécution | Rien. Les artefacts ne reçoivent que des résultats |
Un fichier est reconnu comme modèle à ses premiers octets, pas à son nom. Un
pickle enregistré sous weights.txt est quand même détecté.
Formats et verdicts
| Format | Ce qui est vérifié |
|---|---|
Pickle, joblib, PyTorch (.pt, .pth, .bin, ancien format et zip), tableaux d’objets NumPy | Chaque import du pickle par rapport à une liste d’autorisation. Celle-ci correspond à ce qu’accepte torch.load(weights_only=True) de PyTorch, plus les types NumPy dont un state dict a besoin |
| safetensors | L’en-tête : longueurs, types de données et décalages, et le fait que le fichier ne peut pas aussi être ouvert comme zip ou comme pickle |
| GGUF | Les métadonnées, le modèle de conversation (par rapport à un sous-ensemble sûr de Jinja), et les mêmes vérifications zip et pickle |
| ONNX | Les domaines d’opérateurs personnalisés, et les chemins de données externes qui sortent du répertoire du modèle |
Keras (.keras, .h5) | Les couches Lambda et les objets personnalisés hors de l’espace de noms Keras |
Chaque fichier reçoit un verdict, partagé par toutes les organisations qui détiennent les mêmes octets :
- Sain (
clean) : rien trouvé. - Suspect (
suspicious) : un import hors de la liste d’autorisation (pickle-unknown-import), une classe d’un framework ML connu (pickle-framework-object, voir Pickles de frameworks), une construction de modèle GGUF hors du sous-ensemble sûr, un opérateur personnalisé ONNX, ou un fichier qui peut aussi être ouvert dans un autre format. - Malveillant (
malicious) : un import figurant sur la liste de blocage de l’analyseur (tel queos.systemoubuiltins.eval), ou une couche KerasLambda. - Non analysable (
unscannable) : le fichier n’a pas pu être lu jusqu’au bout : zip endommagé, archive 7z, flux tronqué. - Format vérifié (
format-verified) : un fichier safetensors ou GGUF de plus de 4 Gio dont la vérification de structure a réussi. Voir Au-delà de 4 Gio.
L’analyseur est open source : picklescan pour le parcours du pickle, avec la liste d’autorisation de fremforge par-dessus.
La politique de votre organisation
La politique a trois modes. Définissez-la sous Sécurité › Politiques › Analyse des modèles, ou avec l’API.
| Mode | Ce qui se passe |
|---|---|
| Observation (par défaut) | Chaque fichier est analysé et les fichiers que la politique bloquerait sont listés comme résultats. Rien n’est refusé. |
| Appliquer | Comme en observation, et les fichiers ayant un résultat ouvert sont refusés, comme indiqué dans Où les fichiers de modèles sont trouvés. |
| Désactivé | Les fichiers sont toujours analysés et les résultats toujours listés, mais rien n’est refusé ni compté comme « serait bloqué ». |
En mode Appliquer, la politique bloque un fichier dont le verdict est Malveillant, Non analysable ou Suspect, sauf si le résultat est rejeté. Un fichier Sain ou Format vérifié passe toujours.
À quoi ressemble le mode Appliquer
Un envoi refusé répond 422 avec le verdict, les codes des résultats et le
sha256 du fichier :
{
"error": "model file rejected",
"rule": "suspicious",
"verdict": "suspicious",
"findings": ["pickle-unknown-import"],
"sha256": "d24ec017bdcf45a8737b4a9398e6eea64fa37a5821f473041cb3bc03f8489687"
}Un fichier qui n’a jamais été analysé l’est pendant l’envoi. Cela ajoute au plus quelques secondes pour un fichier jusqu’à 256 Mio. Un fichier safetensors ou GGUF de plus de 256 Mio est vérifié par son en-tête et ses 64 derniers Kio. Tout autre fichier de modèle de plus de 256 Mio est accepté et analysé juste après l’envoi : il apparaît alors comme résultat une minute plus tard au lieu d’être refusé.
Si l’analyseur est injoignable, l’envoi d’un fichier de modèle dans une
organisation en mode Appliquer répond 503 avec Retry-After: 30. Les envois
d’autres fichiers ne sont pas concernés.
Un pull de conteneur qui servirait une couche bloquée répond 403 DENIED, avec
la raison model-malicious, model-suspicious ou model-unscannable et le
condensé de la couche. Une couche de modèle que vous venez de pousser est
analysée en une minute environ. D’ici là, un pull répond 403 avec la raison
model-scan-pending, comme pour une image dont une couche est encore en cours
d’analyse antimalware. Les pulls des runners hébergés par fremforge ne sont pas
retenus pour une analyse en attente.
Uploading failed due to unsupported Content-Type header(s), et
git lfs push --object-id se termine avec le code 0. Un git push normal
échoue bien. La raison se trouve dans le corps de la réponse, et le fichier est
listé comme résultat sous Analyse des modèles.Pickles de frameworks
Certains checkpoints stockent des objets Python entiers, pas seulement des
poids : un modèle YOLO, un fichier TrainingArguments, un pipeline
scikit-learn. En charger un exécute le code de cette classe. L’analyseur les
étiquette pickle-framework-object lorsque chaque import hors de la liste
d’autorisation des poids provient d’un framework connu : modules PyTorch,
Transformers, Ultralytics, scikit-learn, joblib, timm, Lightning, SpeechBrain ou
Diffusers.
Ils sont Suspect comme tout autre import inconnu. Pour les laisser passer
en mode Appliquer, activez Autoriser les pickles des frameworks ML connus
(allow_framework_pickles). Un fichier ayant tout autre résultat reste bloqué.
Rejeter un résultat
Un propriétaire de l’organisation peut rejeter un résultat pour un fichier (un sha256), avec une raison et une date d’expiration : 90 jours par défaut, 365 au plus. Un fichier rejeté passe le mode Appliquer et quitte la liste des résultats. Les rejets sont propres à chaque organisation ; une autre organisation qui détient les mêmes octets n’est pas concernée. Un rejet peut être révoqué à tout moment.
Au-delà de 4 Gio
L’analyseur antimalware, ClamAV, ne peut pas lire plus de 4 Gio d’un même fichier, et fremforge refuse un fichier qu’il ne peut pas analyser. Pour les poids de modèles, qui dépassent souvent 4 Gio, il existe une exception, dans tous les modes : un fichier safetensors ou GGUF dont la vérification de structure réussit est accepté comme Format vérifié. Ces formats ne contiennent que des tenseurs et des métadonnées, et la vérification de structure prouve que le fichier ne contient rien d’autre. Le fichier n’est pas analysé par l’antimalware.
Tout autre fichier de plus de 4 Gio, y compris tout pickle, reste refusé. La limite pour un fichier est de 10 Gio. Les grands modèles sont publiés en fragments de 5 Go au plus, si bien que c’est rarement cette limite qui compte.
Artefacts Actions
À la fin d’une exécution de workflow, les fichiers qu’elle a envoyés avec
actions/upload-artifact sont examinés à la recherche de fichiers de modèles,
qui sont alors analysés. Un résultat est listé sous Analyse des modèles
avec le dépôt, le numéro d’exécution et l’artefact. Les artefacts ne sont
jamais refusés ni supprimés : bloquer un envoi ferait échouer votre workflow
sans aucun moyen de passer outre.
Inventaire IA
Sécurité › Composants › Inventaire IA liste les modèles d’apprentissage automatique que détient votre organisation et ceux que sa CI télécharge. Il est en lecture seule ; la politique et les rejets restent sur la page d’analyse des modèles.
Fichiers de modèles
Une ligne par fichier de modèle (par sha256) trouvé dans votre Git LFS, vos paquets, vos images de conteneurs, les pulls du proxy de dépendances et les artefacts Actions, avec :
- son format et sa taille, et son verdict (Analyse en attente tant qu’il attend son analyse) ;
- sa licence, telle qu’elle est déclarée (voir D’où viennent les licences), avec l’endroit où elle a été déclarée. Un modèle sans licence déclarée affiche Inconnue. fremforge ne devine pas une licence à partir d’un nom de fichier ;
- ce qu’indique son en-tête : nombre de paramètres, types de données des tenseurs et, pour GGUF, l’architecture et le nom du modèle ;
- où il apparaît : chaque dépôt, paquet, image ou artefact où il a été vu.
Chaque fichier de modèle figure aussi dans l’inventaire des composants
sous la forme pkg:generic/<file name>@<sha256>, si bien qu’une recherche de
composant le trouve et que « où est-il utilisé » fonctionne aussi pour les
modèles. Un modèle que votre CI télécharge depuis Hugging Face y figure sous la
forme pkg:huggingface/<org>/<name>@<revision>, au niveau du dépôt dont le
workflow le télécharge.
D’où viennent les licences
fremforge lit une licence uniquement à partir de métadonnées déjà stockées sur fremforge, et ne demande rien à personne d’autre : rien concernant vos modèles ne quitte la plateforme. Dans l’ordre :
- Le fichier lui-même :
general.licensede GGUF, ou une clé de licence dans les métadonnées d’un fichier safetensors. - L’artefact de modèle dont le fichier est une couche, lorsque vous le
poussez dans le registre de votre organisation ou le téléchargez via le
proxy de dépendances. fremforge lit
les petits blobs de métadonnées de l’artefact depuis son propre stockage,
jamais les poids et jamais le registre amont :
Format de paquet Lu ModelPack ( modctl)descriptor.licensesde la configurationKitOps ( kit)le Kitfile : la licensed’une partie de modèle pour cette partie, puismodel.license, puispackage.licenseDocker Model Runner ( docker model,ai/…)general.licensede GGUF dans la configuration du modèle, sinon le fichier de licence de l’artefact (application/vnd.docker.ai.license) : Apache-2.0 et MIT sont reconnues par leur texte, toute autre licence par son titre, tel queLLAMA 3.2 COMMUNITY LICENSE AGREEMENTchacun d’eux l’annotation de manifeste org.opencontainers.image.licenses
L’API renvoie l’endroit où chaque licence a été déclarée dans licence_from,
par exemple ModelPack config descriptor.licenses ; l’inventaire IA l’affiche
à côté de la licence. Une licence provenant d’une image que vous avez poussée
n’est visible que par votre organisation. Un modèle téléchargé avant le
13 octobre 2026 obtient sa licence depuis l’artefact dans l’heure, si
l’artefact est encore stocké.
Modèles que votre CI télécharge
L’analyse quotidienne des workflows lit chaque workflow de la branche par
défaut de chaque dépôt, ainsi que les scripts Python et shell qu’un workflow
exécute depuis le même dépôt (python scripts/fetch.py, bash ci/prepare.sh).
Elle trouve :
| Outil | Détecté sous la forme |
|---|---|
| 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 |
Une référence est épinglée lorsqu’elle ne peut pas changer sous votre build :
un modèle Hugging Face avec un sha de commit complet de 40 caractères comme
revision (revision="<sha>" ou --revision <sha>), et un modèle Ollama,
Docker, KitOps ou ModelPack avec un condensé @sha256:. Une branche ou une
étiquette telle que main ou v1.0 n’est pas épinglée. Chaque référence non
épinglée est un résultat, comme une action épinglée à une étiquette plutôt qu’à
un commit.
Un nom de modèle qui n’est pas écrit dans le fichier, tel que
from_pretrained(model_name), est listé comme Nom défini à l’exécution. Un
HF_ENDPOINT défini dans le même fichier est affiché, car le téléchargement va
alors vers ce point de terminaison au lieu de huggingface.co. Une référence que
vous supprimez disparaît dans les trois jours.
Non détectés : oras pull (un artefact ORAS n’est un modèle que par son type de
média, que la commande ne montre pas ; téléchargé via le proxy de dépendances,
il est classé d’après son manifeste), et le code Python hors des scripts
qu’exécute un workflow.
La licence d’un modèle Hugging Face que votre CI télécharge par son nom est affichée comme Inconnue : fremforge ne contacte ni Hugging Face ni personne d’autre pour la chercher. Un modèle que votre CI télécharge comme artefact OCI via le proxy de dépendances obtient sa licence depuis l’artefact (voir D’où viennent les licences).
ML-BOM
Télécharger le ML-BOM (CycloneDX) sur la page, ou GET /ai/ml-bom,
renvoie l’inventaire sous la forme d’un document CycloneDX 1.6. Chaque fichier
de modèle est un composant de type machine-learning-model avec son sha256, sa
licence déclarée (l’endroit où elle a été déclarée est la propriété
fremforge:model:licence_from), son architecture dans
modelCard.modelParameters.modelArchitecture lorsque l’en-tête en nomme une,
et son format, son verdict, ses surfaces, sa taille, son nombre de paramètres et
ses types de données sous forme de propriétés fremforge:model:*. Seuls les
faits lus dans le fichier sont indiqués.
Les SBOM que votre CI envoie ne sont pas modifiés : ce sont vos fichiers. Le ML-BOM est un document distinct.
Rapport de conformité
Le rapport de conformité comporte une section Inventaire IA : le nombre de fichiers de modèles, les fichiers que votre politique bloque ou bloquerait, les fichiers ayant un résultat rejeté, votre mode d’analyse des modèles, les téléchargements de modèles en CI sans révision fixe, et les fichiers par licence déclarée. Son CSV liste chaque fichier de modèle. La section couvre toujours l’ensemble de l’organisation, y compris dans un rapport portant sur un seul dépôt.
Journal d’audit
| Événement | Quand |
|---|---|
security_policy.model_scan.updated | Le mode ou le paramètre des pickles de frameworks a changé |
security_policy.model_scan.dismissed | Un résultat a été rejeté, ou un rejet renouvelé |
security_policy.model_scan.dismissal_revoked | Un rejet a été révoqué |
registry.pull.blocked avec gate: model | Un pull a été refusé par la politique des modèles |
Un envoi de paquet ou un pull de conteneur refusé figure aussi dans le
journal d’accès au registre, avec la raison
model_policy:<rule>. Le journal d’accès ne couvre pas Git LFS.
API
| Appel | Portée |
|---|---|
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 remplace toute la politique : un champ que vous omettez prend sa valeur
par défaut.
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-policyLe rejet nécessite policy:write, et non findings:write, car un fichier
rejeté passe le mode Appliquer. Le jeton de runner d’un job CI ne le porte
jamais.
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>/dismissConsultez la référence de l’API pour la forme des réponses.
Ce qui n’est pas couvert
- Les fichiers joints aux tickets, aux pull requests et aux versions, et les envois de paquets dont le corps contient plus que le fichier (PyPI, NuGet et autres envois multipart). Ceux-ci sont analysés par l’antimalware comme auparavant.
- Les modèles présents dans les objets Git ordinaires de vos dépôts. Les gros fichiers de modèles ont leur place dans Git LFS.
- Le chargement ou l’exécution d’un modèle, et la détection d’une porte dérobée dans ses poids. Aucun analyseur ne peut distinguer un poids empoisonné d’un poids normal.