Atteignabilité
Un avis de dépendance indique qu’une version de paquet est vulnérable. Il ne dit pas si votre programme peut atteindre le code vulnérable. L’atteignabilité répond à cette question pour Go, à partir des binaires que vous livrez réellement, et à partir du code de votre dépôt pour JavaScript et TypeScript (npm) et, au niveau des imports, pour Python (PyPI) et Java et Kotlin (Maven). Elle vous montre pourquoi. Pour Go, elle analyse aussi le code propre à votre dépôt (mode source) ; voir ci-dessous.
Ce qu’elle vérifie
Go : les binaires que vous livrez
Lorsque fremforge réanalyse les images de conteneurs du registre de votre organisation, chaque binaire Go qui a un résultat de vulnérabilité est lu par govulncheck en mode binaire. govulncheck lit la liste des modules et la table des symboles du binaire et les compare avec la base de vulnérabilités Go, qui nomme les fonctions exactes concernées par chaque avis.
- Rien dans votre image n’est compilé ni exécuté. Le binaire est lu comme un fichier.
- Aucun service n’est appelé. La base de vulnérabilités est la copie propre à fremforge des enregistrements OSV Go, rafraîchie chaque jour.
- govulncheck (BSD-3-Clause) est épinglé sur une release et mis à jour via Renovate.
Providers Terraform et OpenTofu : les binaires que vos fichiers de verrouillage épinglent
Un résultat de provider Terraform ou OpenTofu est un module Go intégré au binaire du provider que votre .terraform.lock.hcl épingle. fremforge lit ce binaire avec govulncheck en mode binaire, comme pour un binaire Go dans une image, et le résultat reçoit le verdict : atteignable lorsqu’une fonction concernée est liée dans le provider, non atteignable lorsque le module (ou son paquet) est dans le binaire mais qu’aucune des fonctions nommées par l’avis ne l’est.
- Les releases de providers sont compilées sans table des symboles (
-ldflags=-s). La table des fonctions que Go conserve à l’exécution est toujours présente ; fremforge restaure le seul symbole dont govulncheck a besoin pour la lire, et le résultat est le même que pour le même provider compilé avec ses symboles. Si ce symbole est introuvable, le verdict resteunknown(no_symbol_table). - Chaque version de provider est analysée une seule fois, pour toutes les organisations qui l’épinglent, à partir de la copie que conserve le miroir de providers de fremforge. Rien de votre dépôt n’est utilisé.
- Un binaire de provider de plus de 512 Mio (par exemple
hashicorp/aws) garde le verdictunknown(engine_failed).
Go : le code de votre dépôt
Lorsqu’une analyse des dépendances trouve des modules Go vulnérables, fremforge exécute aussi govulncheck en mode source sur le commit analysé. Il vérifie les types de vos modules et construit un graphe d’appels à partir de vos paquets main, afin de pouvoir montrer le chemin d’appel d’une ligne de votre dépôt jusqu’à la fonction concernée, même lorsqu’aucune image n’a été construite à partir de ce commit.
Cela nécessite la commande go, qu’un dépôt peut tenter d’influencer. fremforge l’exécute de façon que rien de votre dépôt ne soit exécuté et que rien ne soit récupéré autrement que via le proxy de dépendances de votre organisation. La conception a été relue avant sa mise en service.
- Rien de votre dépôt ne s’exécute. Pas de
go generate, pas de tests, pas de build, pas de cgo (CGO_ENABLED=0), pas de-toolexec. Seul le vérificateur de types de la chaîne d’outils Go lit votre code. - Aucune autre chaîne d’outils. Une ligne
toolchainest ignorée (GOTOOLCHAIN=local). Une lignegoplus récente que le Go de fremforge rend le moduleunknown(build_failed). fremforge maintient son Go à jour via Renovate. - Pas de gestion de versions ni d’autre réseau. Les modules proviennent uniquement du proxy de dépendances de votre organisation, via un relais qui ajoute l’identifiant d’analyse de fremforge, si bien que votre politique de paquets s’applique. Il n’y a pas de repli vers un hébergeur de gestion de versions. Si votre organisation a désactivé son proxy de dépendances, l’analyse lit les modules depuis le cache d’analyse propre à fremforge sur le même proxy. Ce cache ne contient que des modules publics de
proxy.golang.org, chacun vérifié auprès desum.golang.org, et n’utilise aucun identifiant à vous, si bien que votre politique de paquets ne s’applique pas à ces lectures. Une organisation dont le proxy refuse un module en vertu de sa politique obtientunknownpour ce module ; l’analyse ne contourne jamais votre proxy. - Les modules privés ne sont jamais demandés. Un module sous
frem.sh, ou correspondant à un motifGOPRIVATE,GONOPROXYouGONOSUMDBque votre dépôt déclare (dans ses workflows CI,go.env,.envrc,.envouMakefile), est refusé avant qu’une requête ne quitte l’analyse. Son chemin n’est jamais envoyé au proxy de dépendances, àproxy.golang.orgni àsum.golang.org, et tout ce qui en a besoin estunknown(build_failed). La même règle s’applique aux recherches de licence, d’âge de version et de graphe de dépendances Go. Voir ce qui parvient à Google pour les modules publics. - Vos fichiers de configuration ne sont pas lus. Un fichier
GOFLAGS,go.envou.config/go/envversionné, un répertoirevendor/et un.netrcsont ignorés. Les liens symboliques ne sont pas suivis. - Refusés avant l’analyse, en
unknown(build_failed) : un module régi par ungo.work(il change le code compilé), unreplacevers un chemin absolu ou hors du dépôt, et un chemin de module qui nomme directement un dépôt de gestion de versions (….git). - Séparée des identifiants de fremforge. L’analyse s’exécute dans son propre conteneur, qui ne détient aucun identifiant et n’atteint rien d’autre que le relais. Elle a ses propres limites de mémoire et de CPU.
- Limites : 20 modules, 20 000 fichiers et 128 Mio de votre dépôt par analyse, 512 Mio de téléchargements de modules, et un budget de temps de 3 minutes par analyse. Au-delà d’une limite, le verdict est
unknown. - Les tests (
_test.go) ne sont pas analysés, comme en mode binaire. La bibliothèque standard n’est jugée qu’en mode binaire : le mode source la lirait dans la version de Go de fremforge, pas dans la vôtre. - Elle ne fait jamais échouer l’analyse et ne modifie jamais le statut
fremforge/dep-scan.
JavaScript et TypeScript : le code de votre dépôt
Lorsqu’une analyse des dépendances trouve des paquets npm vulnérables, fremforge exécute Jelly, un analyseur statique de graphe d’appels pour JavaScript et TypeScript issu de l’université d’Aarhus, sur le commit analysé. Il vérifie si votre code charge le paquet vulnérable, directement ou via d’autres paquets.
Jelly lit votre code. Il ne l’exécute jamais : rien n’est installé, aucun code de paquet ne s’exécute et aucun script d’installation ne s’exécute.
Les paquets sur le chemin d’un paquet vulnérable sont récupérés par nom et version et décompressés comme des données. Chacun est vérifié par rapport au hachage d’intégrité de votre fichier de verrouillage, et le
.npmrcde votre dépôt n’est pas utilisé. Ils passent par le proxy de dépendances de votre organisation (voir D’où viennent les fichiers de paquets).Chaque archive est lue en flux. Elle est vérifiée par rapport à son hachage d’intégrité à mesure qu’elle arrive, et seuls les fichiers que Jelly lit sont écrits :
.js,.mjs,.cjs,.jsx,.ts,.mts,.cts,.tsxet.json. Les binaires natifs et autres gros fichiers sont ignorés sans être conservés, si bien qu’un paquet tel queonnxruntime-node(108 Mio) est analysé comme n’importe quel autre. Les fichiers ne sont conservés que lorsque le hachage correspond.Limites : une archive d’au plus 512 Mio, 2 Gio une fois décompressée, 100 000 entrées, 10 000 fichiers écrits et 16 Mio par fichier écrit. L’ensemble des paquets disposés pour un fichier de verrouillage peut occuper jusqu’à 96 Mio de fichiers écrits. Un paquet qui dépasse une limite rend
unknown(build_failed) les résultats qui en ont besoin. Un fichier ignoré pour sa taille ne permet jamais à un résultat de devenirunreachable.Fichiers de verrouillage pris en charge :
Fichier de verrouillage Versions Intégrité vérifiée par rapport à package-lock.json,npm-shrinkwrap.jsonversion de fichier 2 ou 3 (npm 7 et ultérieur) le fichier de verrouillage pnpm-lock.yaml5.x, 6 et 9 le fichier de verrouillage yarn.lock, classiqueyarn 1 le fichier de verrouillage ( integrity, ou le SHA-1 dans une ancienne URLresolved)yarn.lock, berryyarn 2 et ultérieur ( __metadataversion 4 et ultérieure)l’intégrité que votre registre publie pour cette version, car le checksumde berry porte sur l’archive du cache propre à yarn et non sur l’archive npm. Un paquet dont l’intégrité publiée ne peut pas être lue n’est pas récupéré.bun.lockformat texte (bun 1.2 et ultérieur) le fichier de verrouillage Pour yarn classique, qui n’enregistre pas d’espaces de travail, le
package.jsonà côté du fichier de verrouillage et sesworkspacessont lus aussi.bun.lockb, l’ancien format binaire de bun, n’est pas lu : c’est une image mémoire non documentée qui a changé d’une version de bun à l’autre. Ses résultats sontunknown, et les preuves indiquent pourquoi. Les autres fichiers de verrouillage donnentunknown.Jelly (BSD-3-Clause) s’exécute dans le job d’analyse de fremforge et n’appelle aucun service. Il est épinglé sur une release et mis à jour via Renovate.
L’analyse a un budget de temps de 2 minutes par analyse. Elle ne fait jamais échouer l’analyse. Elle ne modifie le statut
fremforge/dep-scanque si votre organisation choisit de ne pas bloquer sur les résultats non atteignables.
Python, Java et Kotlin : les imports de votre dépôt
Lorsqu’une analyse des dépendances trouve des paquets PyPI ou Maven vulnérables, fremforge vérifie si votre code les importe. C’est uniquement au niveau des imports : cela vous indique qu’un paquet est chargé, jamais que le code vulnérable s’exécute, et cela ne donne jamais unreachable.
Python. Vos fichiers
.pysont analysés avec le moduleastpropre à Python, qui ne fait qu’analyser la syntaxe. Rien n’est importé ni exécuté. fremforge lit les instructionsimportetfrom … import, ainsi que les appelsimportlib.import_module(...)et__import__(...)avec un nom littéral. Un fichier qui ne s’analyse pas avec le Python actuel, comme du code Python 2, est lu ligne par ligne pour ses instructions d’import. Les imports relatifs (from . import x) désignent vos propres modules et sont ignorés, de même que les environnements virtuels (.venv,venv,site-packages) et les liens symboliques.Python, via d’autres paquets. Lorsque votre code n’importe pas lui-même un paquet vulnérable, fremforge suit les paquets que votre code importe : il lit leur code dans leurs wheels de la même manière, en partant des modules que votre code importe et en ne suivant que leurs propres imports. Si du code atteint de cette façon importe le paquet vulnérable, le résultat est Importé, et ses preuves montrent le chemin : votre import, puis l’import dans chaque paquet, par exemple
idnaviarequests. Un module que rien ne charge (comme lehelp.pyd’un paquet) et les imports sousif TYPE_CHECKING:ne sont pas suivis. Ce parcours va au plus à trois paquets de profondeur et lit au plus 40 wheels d’au plus 16 Mio chacune par analyse. Il ne fait qu’ajouter des verdicts Importé ; un chemin qu’il ne peut pas suivre laisse le résultatunknown.Java, Kotlin, Groovy et Scala. fremforge lit les instructions
import, les noms de classes pleinement qualifiés dans le code, etClass.forName("…")etloadClass("…")avec un nom littéral. Les commentaires et les chaînes littérales sont ignorés, de même quebuild/ettarget/. Rien n’est compilé.Java et Kotlin, via d’autres paquets. Lorsque votre code n’importe pas lui-même un paquet Maven vulnérable, fremforge suit les paquets que votre code importe, dans leurs classes compilées. Il part des classes que votre code importe (chaque classe du paquet pour un import générique
import a.b.*). De là, il suit les classes qu’elles utilisent : chaque classe dont une méthode est appelée, qui est instanciée, vers laquelle une conversion est faite ou dont un champ est lu, ainsi que chaque superclasse et interface. Elles sont lues dans le pool de constantes de chaque fichier de classe. Si une classe atteinte de cette façon utilise une classe du paquet vulnérable, le résultat est Importé. Ses preuves montrent le chemin : votre import, puis le fichier source et la ligne dans chaque paquet où le suivant est utilisé. Par exemple,commons-textviacommons-configuration2. Une classe que rien n’atteint n’est pas suivie, pas plus qu’une classe chargée par son nom à partir d’une chaîne. Les fichiers de classe sont lus comme des données : aucune classe n’est chargée ni exécutée, et aucune JVM n’intervient. Ce parcours va au plus à trois paquets de profondeur et lit au plus 40 JAR d’au plus 32 Mio chacun par analyse, un à la fois. Il ne fait qu’ajouter des verdicts Importé ; un chemin qu’il ne peut pas suivre laisse le résultatunknown.Les noms que fournit un paquet proviennent de la liste des fichiers de son archive publiée, lue comme des données :
- PyPI : la wheel de cette version. Un paquet d’espace de noms se résout vers son premier véritable paquet, par exemple
google.protobufpourprotobuf. - PyPI, une release sans wheel (sdist uniquement) : l’archive source, lue comme des données et jamais installée.
setup.pyn’est ni exécuté ni lu. Les noms proviennent des listes de paquets desetup.cfgou depyproject.toml(setuptools, hatch, flit et poetry), ou sinon des répertoires contenant un__init__.py, à la racine, soussrc/ou sous un répertoire tel quelib/. Ainsi,pysaml2fournitsaml2etpycryptofournitCrypto.PKG-INFOdoit nommer le même projet. Les archives jusqu’à 16 Mio sont lues (.tar.gz,.tar,.zip). Une release absente de tout index, ou un sdist qui ne nomme rien (par exemple un.tar.bz2), se rabat sur le nom du projet, si bien querequestsvaut pourimport requests; lemappingdes preuves l’indique. - Maven : le JAR sur Maven Central, et les paquets Java dans lesquels se trouvent ses classes.
Pour une wheel ou un JAR, seul le répertoire de l’archive est récupéré, avec des requêtes HTTP partielles, si bien qu’une grosse archive ne coûte que quelques kilo-octets. Le parcours via d’autres paquets lit en entier la wheel ou le JAR de chaque paquet qu’il suit, et un sdist est toujours lu en entier. Rien n’est installé, compilé ni exécuté.
- PyPI : la wheel de cette version. Un paquet d’espace de noms se résout vers son premier véritable paquet, par exemple
Votre dépôt n’est pas compilé. L’atteignabilité Java avec graphes d’appels, comme l’analyse expérimentale des JAR d’osv-scanner ou
jdeps, nécessite un JAR compilé. Une analyse de pull request ne compile pas votre projet : ces outils ne sont donc pas utilisés.L’analyse a un budget de temps de 1 minute par écosystème et par analyse, en parallèle de l’analyse npm. Elle ne fait jamais échouer l’analyse et ne modifie jamais le statut
fremforge/dep-scan.
D’où viennent les fichiers de paquets
Les archives npm, les index et wheels PyPI et les JAR Maven sont lus via le proxy de dépendances de votre organisation, sur une route interne que seul le service d’analyse de fremforge peut atteindre. La politique de paquets, le cache, le plafond de stockage et le débit amont de votre organisation s’y appliquent donc comme à tout autre pull, et ce que l’analyse récupère est mis en cache pour vos builds.
- Si votre politique refuse un paquet (par exemple malware, release trop récente ou règle de licence), l’analyse ne le récupère nulle part ailleurs. Les résultats qui en ont besoin sont
unknown(build_failed), et les preuves nomment la décision du proxy. - Si votre organisation a désactivé le proxy de dépendances, ou si le proxy ne peut pas répondre, les fichiers sont lus depuis les registres publics (npmjs, PyPI, Maven Central), comme auparavant.
- Les fichiers que récupère l’analyse comptent dans le stockage du proxy de votre organisation comme tout autre fichier en cache.
Les niveaux
Un verdict a un niveau : ce que le moteur a pu voir.
| Niveau | Signification |
|---|---|
function | L’avis nomme les fonctions concernées, et le moteur a cherché un chemin vers elles. C’est ce que donne govulncheck pour Go. |
import | L’avis ne nomme aucune fonction concernée. Le moteur peut seulement dire si le paquet vulnérable est chargé. |
Les avis npm ne nomment presque jamais les fonctions concernées. Lors de la mesure du 2026-10-12, aucun des 230 287 enregistrements npm de la base OSV ne le faisait. Les verdicts JavaScript et TypeScript sont donc au niveau import : ils vous indiquent que votre code charge le paquet vulnérable, pas qu’il appelle le code vulnérable. Si un avis npm nomme des fonctions concernées, Jelly les vérifie au niveau fonction.
Les verdicts Python et Maven sont toujours au niveau import. La même mesure a trouvé des données de fonctions concernées dans 2 des 26 221 enregistrements PyPI et dans aucun des 7 190 enregistrements Maven.
Les trois verdicts
Chaque résultat de dépendance et d’image porte l’un de ces verdicts :
| Verdict | Go (govulncheck) | JavaScript et TypeScript (Jelly) | Python, Java et Kotlin (niveau import) |
|---|---|---|---|
| Atteignable | Une fonction concernée est liée dans le binaire. L’éditeur de liens Go supprime le code que rien ne peut appeler : une fonction liée est donc une fonction que votre programme peut atteindre. Le marqueur nomme la fonction, par exemple html.Renderer.renderLink. En mode source : un chemin d’appel depuis votre code atteint une fonction concernée ; le marqueur donne le fichier et la ligne du dernier appel dans votre dépôt avant que le chemin n’entre dans la dépendance, par exemple votre handler plutôt que main. Le chemin complet figure dans les preuves. | Au niveau import, affiché comme Importé : votre code charge le paquet dans la version vulnérable, directement ou via d’autres paquets. Au niveau fonction, affiché comme Atteignable : un appel dans votre code peut atteindre une fonction concernée. Le marqueur donne le fichier et la ligne dans votre dépôt, par exemple src/session.ts:12. | Affiché comme Importé : votre code importe un module ou un paquet Java que fournit le paquet vulnérable. Le marqueur donne le fichier et la ligne de l’import, par exemple app/main.py:2. |
Non atteignable (unreachable) | Le module est dans le binaire dans une version concernée, l’avis nomme les fonctions concernées, govulncheck est allé jusqu’au bout, et aucune de ces fonctions n’est dans le binaire. En mode source : le module est utilisé dans une version concernée, govulncheck n’a trouvé aucun chemin d’appel vers une fonction concernée, et rien de ce que govulncheck ne peut pas suivre (réflexion, fichiers pour d’autres plateformes, cgo, plugins) ne se trouve dans votre code. | L’avis nomme les fonctions concernées, Jelly est allé jusqu’au bout avec chaque paquet sur le chemin disponible, et aucun appel de votre code n’en atteint une. Sans ces données de fonctions, un paquet que votre code ne charge jamais est unknown, pas unreachable. | Jamais. Les imports dans d’autres paquets ne sont suivis que dans la mesure où ils nomment un module ou une classe littéralement : un paquet que votre code ne semble pas charger peut donc tout de même être chargé. |
| Inconnu | Tout le reste. C’est la valeur par défaut, et elle n’est jamais considérée comme sûre. | Idem. | Idem, y compris chaque paquet que votre code n’importe pas. |
unreachable exige toutes ses conditions. Une donnée manquante donne toujours unknown, avec la raison :
| Raison | Pourquoi |
|---|---|
no_symbol_data | L’avis ne nomme pas les fonctions concernées : l’absence de chemin ne prouve donc rien. Pour npm, c’est la raison habituelle lorsque votre code ne charge pas le paquet. Pour Python et Maven, c’est la raison chaque fois que votre code n’importe pas le paquet. |
no_symbol_table | Go : le binaire a été compilé avec -ldflags=-s. Ses symboles ne peuvent pas être lus, si bien qu’aucun des deux verdicts n’est acquis. Compilez sans -s (garder -w ne pose pas de problème) pour obtenir des verdicts. Providers Terraform : le symbole que fremforge restaure est introuvable dans ce binaire. |
build_failed | Go en mode source : le module a été refusé avant l’analyse (voir ci-dessus), n’a pas pu être chargé, ou un module n’a pas pu être récupéré depuis votre proxy de dépendances. npm : le fichier de verrouillage n’est pas l’un des formats pris en charge, ou un paquet sur le chemin n’a pas pu être récupéré, dépassait une limite ou ne correspondait pas à son hachage d’intégrité. Python et Maven : la wheel, le sdist ou le JAR n’a pas pu être lu, par exemple un artefact ne contenant qu’un POM ou absent de Maven Central. |
dynamic_code | Votre code charge des modules ou des classes par des noms calculés : l’absence de chemin d’appel ou d’import ne prouve donc rien. npm : require(name) et import(name). Python : importlib.import_module(name) et __import__(name). Java : Class.forName(name) et loadClass(name). Go en mode source : le paquet plugin ou //go:linkname. |
reflection | Go en mode source : votre code appelle des méthodes via reflect (MethodByName, Method, Call), ou utilise text/template ou html/template lorsque l’avis nomme des méthodes. govulncheck ne peut pas suivre ces appels : l’absence de chemin d’appel ne prouve donc rien. |
build_constraints | Go en mode source : un fichier que l’analyse n’a pas chargé importe d’autres paquets. L’analyse charge votre code comme pour Linux sur amd64 sans cgo : un fichier _windows.go, un fichier dont la ligne //go:build exclut Linux, ou un fichier cgo ne figure pas dans le graphe d’appels, et un appel fait uniquement à cet endroit serait manqué. Un paquet qui ne se compile qu’avec cgo (par exemple une interface de bureau sur une boîte à outils exclusivement cgo) est exclu de l’analyse au lieu de la faire échouer : le reste du module est analysé, et chaque module que ce paquet importe reçoit cette raison plutôt que unreachable. |
timeout | L’analyse a dépassé son budget de temps (5 minutes par image pour Go, 2 minutes par analyse pour npm, 1 minute par écosystème et par analyse pour Python et Maven). |
engine_no_match | Go : govulncheck n’a pas signalé cette version de module comme concernée par l’avis, alors que l’analyseur d’images l’a fait. Aucun des deux n’est considéré comme plus fiable que l’autre. |
oom_killed | Go en mode source : l’analyse a manqué de mémoire et a été arrêtée : rien n’a donc été décidé. |
engine_failed | L’analyse n’a pas pu s’exécuter sur ce binaire ou ce dépôt. |
not_analysed | Aucun verdict n’a été calculé : le résultat n’a pas été analysé depuis l’ajout de l’atteignabilité, ou un résultat Go n’a aucune image construite à partir de son commit. |
unsupported_ecosystem | Un résultat de dépendance hors de Go, npm, PyPI, Maven et des providers Terraform, par exemple NuGet ou Cargo. |
Résultats de dépendances
Go. Un résultat de dépendance Go reçoit son verdict en mode binaire, à partir d’une image construite à partir d’exactement ce commit. Là où le mode source est activé (pour l’instant, dans les organisations internes de fremforge), il en reçoit aussi un à chaque analyse, avec le chemin d’appel dans votre dépôt. Étiquetez vos images avec les deux labels OCI standard :
org.opencontainers.image.source=https://frem.sh/<org>/<repo>
org.opencontainers.image.revision=<full commit sha>docker/metadata-action définit les deux. Le verdict ne s’applique qu’au commit que nomme l’image et aux versions de modules avec lesquelles les binaires ont été compilés. Si l’un des binaires Go de cette image n’a pas pu être lu, le résultat de dépendance reste unknown. Si plusieurs binaires contiennent le module, le résultat est atteignable dès que l’un d’eux l’atteint.
Lorsque les deux modes jugent le même résultat au même commit, aucun n’efface l’autre : un unknown de l’un ne remplace jamais un verdict de l’autre, et non atteignable de l’un ne remplace jamais atteignable de l’autre. Les preuves du mode source portent "mode": "source".
npm, PyPI et Maven. Les verdicts arrivent avec l’analyse des dépendances elle-même : chaque analyse de pull request, et l’analyse de votre branche par défaut après chaque push (au plus une fois par heure). Un verdict appartient toujours au commit analysé. Si le même paquet et le même avis apparaissent dans plusieurs fichiers de verrouillage, le résultat est atteignable dès que l’un d’eux l’atteint.
Comment fremforge l’utilise
Classement. Les listes de résultats Dépendances et Images de conteneurs sont triées en mettant d’abord les atteignables, puis les inconnus, puis les non atteignables. Au sein de chaque groupe, elles sont triées par gravité, puis les vulnérabilités connues comme exploitées avant les autres, puis par EPSS. Un résultat est connu comme exploité lorsque l’un de ses identifiants CVE, le sien ou les alias de son avis, est listé comme exploité par l’EU Vulnerability Database de l’ENISA ou par le catalogue Known Exploited Vulnerabilities de la CISA. Les deux listes affichent un badge Exploitation connue dont l’infobulle nomme la source : UE (ENISA EUVD), États-Unis (CISA KEV), ou les deux, et au-dessus de chaque liste, Exploitation connue uniquement n’affiche que ces résultats (le paramètre ?known_exploited=true de l’API des résultats). Les deux listes sont rafraîchies chaque jour. Un résultat Importé est classé avec les atteignables. Le même ordre est utilisé par :
- l’onglet Recommandations, où une carte (une montée de version d’un paquet dans un dépôt) prend le verdict le plus atteignable des résultats qu’elle corrige et est marquée Atteignable ou No call path ;
- le bloc Top fixable today du résumé hebdomadaire, qui affiche le verdict de chaque ligne ;
- l’API des résultats avec
?sort=risk.
Alertes. Lorsqu’un résultat de dépendance ouvert devient atteignable, ou devient connu comme exploité, votre SIEM reçoit immédiatement un événement d’audit et les contacts de sécurité de l’organisation reçoivent un e-mail, une fois par résultat : voir Alertes sur les résultats. Les vulnérabilités connues comme exploitées, et les résultats atteignables de gravité HIGH ou CRITICAL, sont envoyés immédiatement par e-mail. Les résultats atteignables de gravité MEDIUM ou LOW rejoignent le récapitulatif quotidien des alertes de chaque personne, ou sont envoyés immédiatement à qui a désactivé le récapitulatif. La liste de démarrage sur Sécurité › Vue d’ensemble montre lesquelles de vos images contiennent des binaires Go sans symboles.
Le groupe probablement non exploitable. Les résultats non atteignables passent dans un groupe replié en bas de la liste, intitulé avec leur nombre. Ils restent visibles, sélectionnables et ignorables, et ils sont comptés dans chaque total.
Rien n’est masqué ni ignoré. Un verdict n’écrit jamais de dérogation. Il ne modifie le SLA d’un résultat que si votre organisation active le multiplicateur de SLA. Par défaut, le statut de commit (fremforge/dep-scan, fremforge/image-scan) ne lit pas les verdicts : ce qui bloque une fusion ou un pull ne change pas. La seule exception est la politique optionnelle de ne pas bloquer les pull requests sur les résultats non atteignables, qui ne s’applique qu’à fremforge/dep-scan.
Multiplicateur de SLA (optionnel)
Un propriétaire de l’organisation peut allonger le SLA par gravité des résultats non atteignables, sous Sécurité › Prioriser › Délai de correction (SLA) (/<org>/_admin/security/prioritise/sla), ou avec unreachable_sla_multiplier sur PUT /orgs/{org}/security/sla-policy. La valeur est un nombre entier de 1 à 4. La valeur par défaut, 1, correspond à la désactivation.
- Seuls les résultats
unreachablesont allongés. Les résultats atteignables et inconnus gardent toujours le SLA complet, de même qu’un résultat qui n’a pas été analysé. - Un résultat connu comme exploité (listé par l’UE (ENISA EUVD) ou les États-Unis (CISA KEV), voir Classement ci-dessus) n’est jamais allongé, quelle que soit sa gravité et quel que soit son verdict.
- Un résultat critique dont le score EPSS est dans les 10 % les plus élevés (percentile 0,90 ou plus) n’est jamais allongé, quel que soit son verdict.
- L’échéance est toujours comptée à partir de la première détection du résultat. Lorsqu’une analyse ultérieure trouve un chemin d’appel, ou ne peut pas trancher, le SLA complet s’applique de nouveau immédiatement, si bien que le résultat peut passer directement en dépassement de SLA. La pastille SLA indique alors pourquoi.
- Avec le multiplicateur activé, une pastille SLA allongée affiche un petit
×2(ou×3,×4) et une infobulle avec les deux échéances. Le nombre de dépassements de SLA du résumé hebdomadaire, la tuile de la vue d’ensemble et l’événement SIEMcode-security.sla.breachedutilisent les mêmes échéances allongées. - Le multiplicateur ne modifie que les échéances. Il ne change pas ce qui bloque une pull request, et il ne masque rien.
Chaque modification est auditée sous code-security.sla_policy.updated, avec la valeur précédente.
Ne pas bloquer sur les résultats non atteignables (optionnel)
Par défaut, chaque résultat de dépendance égal ou supérieur à votre seuil de blocage bloque la pull request, quel que soit son verdict. Un propriétaire de l’organisation peut choisir qu’un résultat non atteignable ne bloque pas fremforge/dep-scan. C’est désactivé tant que vous ne l’activez pas.
Activez-le sous Sécurité › Prioriser › Vérifications des pull requests (/<org>/_admin/security/prioritise/pr-checks), ou avec PUT /orgs/{org}/security/dep-scan-unreachable-policy (voir API). Vous pouvez le limiter à une liste de dépôts ; une liste vide signifie tous les dépôts de l’organisation.
Ce que cela change, et ce que cela ne change jamais :
- Seul un
unreachableétayé par des preuves est admissible. Le verdict du résultat pour le commit de tête de la pull request doit être non atteignable, calculé par un moteur avec sa version et les fonctions concernées de l’avis qu’il a vérifiées. Aujourd’hui, cela concerne Go ainsi que JavaScript et TypeScript. Pour Go, govulncheck donne le verdict en mode binaire une fois qu’une image construite à partir du commit de tête de la pull request a été analysée ; un verdict qui arrive après l’analyse s’applique à partir de l’analyse suivante de la pull request. Là où le mode source est activé (pour l’instant, dans les organisations internes de fremforge), il en donne aussi un lors de l’analyse de la pull request elle-même. Pour JavaScript et TypeScript, Jelly le donne lorsque l’avis nomme des fonctions concernées. Les verdicts Python, Java et Kotlin sont au niveau import et ne sont jamaisunreachable: ils bloquent donc toujours. unknownbloque toujours. Il n’existe aucune option pour ne pas bloquer surunknown, et un résultat qui n’a pas été analysé estunknown.- Les malwares et les vulnérabilités connues comme exploitées bloquent toujours, quel que soit le verdict : un avis
MAL-ou un avis marqué comme malveillant, et une vulnérabilité dont l’un des identifiants CVE est listé comme exploité par l’UE (ENISA EUVD) ou les États-Unis (CISA KEV). Si fremforge ne peut pas lire ces informations pendant une analyse, rien n’est laissé passer lors de cette analyse. - Rien n’est masqué. Le résultat est stocké, listé, compté dans chaque total et donne lieu à des alertes comme tout autre. Il n’est pas ignoré, et son SLA est inchangé (sauf si le multiplicateur de SLA est aussi activé).
- Pull requests uniquement. L’analyse de votre branche par défaut est enregistrée exactement comme avant.
- Les correspondances avec la liste de refus et le seuil de blocage sont inchangés. Un résultat sous le seuil n’a jamais bloqué, et un composant refusé bloque toujours.
Le blocage revient. Lorsqu’une analyse ultérieure de la pull request trouve un chemin d’appel, ou ne peut pas trancher, le résultat bloque de nouveau. Un verdict calculé en dehors d’une analyse (govulncheck sur une image), ainsi que la désactivation ou la restriction de la politique, sont pris en compte dans l’heure : fremforge revérifie chaque pull request qui est passée grâce à la politique et recalcule son statut, son commentaire et son événement d’audit. Il en va de même lorsqu’un résultat devient connu comme exploité.
Ce que montre la pull request. Le statut fremforge/dep-scan indique combien de résultats n’ont pas bloqué, par exemple 2 finding(s); 1 at or above critical with no call path, not blocking (org policy). Le commentaire marque chacun de ces résultats not blocking et les liste sous Not blocking: no call path (org policy) avec le moteur, sa version, la date de la base de vulnérabilités et les fonctions qu’il a vérifiées. Un résultat sans chemin d’appel qui bloque malgré tout est listé sous Blocking although no call path was found, avec la raison (malware ou exploitation connue).
Audit. Chaque modification de la politique est auditée sous code-security.deps.unreachable_policy.updated, avec la nouvelle valeur et la précédente, si elle a changé, et si elle vient de la console ou de l’API. Chaque analyse de pull request pendant que la politique s’applique enregistre, dans son événement code-security.dep_scan.ingest, chaque résultat qui n’a pas bloqué (unreachable_not_blocking, avec le moteur, la version et la date de base sur lesquels il s’est appuyé) et chaque résultat qui a bloqué malgré tout (unreachable_blocked_anyway, avec why). Une revérification porte trigger: unreachable_policy_recheck et la raison. Les deux parviennent à vos points de terminaison SIEM.
Quand un verdict change : reachability_changed
Une dérogation ou une exception peut reposer sur un verdict. Lorsque vous ignorez un résultat de dépendance ou d’image dont le verdict est non atteignable, ou demandez une exception pour lui, fremforge enregistre le verdict sur lequel elle s’appuie : le moteur, sa version, la date de la base et le commit ou l’image sur lequel il a été calculé. Le panneau de dérogation l’indique.
Chaque heure, fremforge vérifie ces dérogations et exceptions. Lorsqu’une analyse postérieure à la dérogation trouve le même résultat reachable, ou ne peut pas trancher (unknown) :
- une exception est révoquée avec la raison
reachability_changed; - une simple dérogation est révoquée : le résultat se rouvre, et une dérogation à l’échelle du dépôt qu’elle avait créée cesse de s’appliquer aux analyses suivantes ;
- le résultat donne de nouveau lieu à une alerte, via la même alerte de sécurité qu’une dérogation expirée ;
- les deux sont des événements d’audit (
code-security.exception.revoked, oucode-security.deps.dismissal_revokedetcode-security.images.dismissal_revoked) avec le verdict sur lequel elle s’appuyait et le nouveau, et ils parviennent à vos points de terminaison SIEM. L’événement du résultat l’indique aussi :security.finding.exception_revoked, ou pour une simple dérogationsecurity.finding.dismissal_revoked, tous deux avectransition_reasonreachability_changed.
Un résultat qui n’a pas été réanalysé garde sa dérogation : l’absence de nouvelle analyse n’est pas un nouveau verdict. Une dérogation faite alors que le verdict n’était pas unreachable n’enregistre rien et n’est jamais révoquée par cette vérification, et les dérogations antérieures à l’ajout de ce mécanisme ne sont pas vérifiées. Si vous avez ignoré un résultat pour une raison qui ne dépend pas de l’atteignabilité, ignorez-le de nouveau après la révocation ; le verdict n’étant alors plus unreachable, la nouvelle dérogation reste.
Signaux de tri SAST
Les résultats SAST n’ont pas de graphe d’appels : trois signaux déterministes les classent donc à la place. Aucun modèle d’IA n’intervient.
| Signal | Ce que c’est |
|---|---|
| Emplacement | Déduit du chemin du fichier : code de test (test/, __tests__/, *.test.ts, *_test.go, test_*.py, *Test.java et similaires), exemples et documentation, code tiers (vendor/, third_party/, node_modules/) et code généré (*.pb.go, *.min.js, gen/, zz_generated* et similaires). |
| Chemin de propagation | Les règles en mode taint d’OpenGrep suivent les données depuis une source, comme une entrée utilisateur, jusqu’au puits. Une correspondance avec une trace est marquée Chemin de propagation et est classée au-dessus d’une correspondance par motif seul. Seul le fait est stocké, jamais la trace. |
| Souvent écarté ici | Des personnes ont ignoré la même règle sur au moins 2 autres résultats de ce dépôt, ou 5 de l’organisation, au cours des 90 derniers jours. Les bots, la résolution automatique et les autres acteurs système ne comptent pas. |
Un résultat dans du code de test, d’exemple, tiers ou généré, ou marqué souvent écarté ici, passe dans un groupe replié Confiance plus faible en bas de la liste SAST, intitulé avec leur nombre. Un chemin de propagation ne fait pas sortir un résultat de ce groupe : une correspondance taint dans un fichier de test reste dans un fichier de test. Au sein de chaque groupe, les résultats sont triés par gravité, puis par chemin de propagation, puis par ancienneté, les plus anciens d’abord.
Comme le groupe probablement non exploitable, les résultats de confiance plus faible ne sont jamais masqués ni ignorés : ils gardent leur SLA complet, sont comptés partout, et peuvent être sélectionnés, corrigés et ignorés comme les autres. L’API des résultats SAST renvoie les signaux sous la forme d’un objet triage sur chaque résultat, et ?sort=risk liste les résultats dans cet ordre sur l’ensemble des pages.
Commentaire de pull request. Le commentaire de l’analyse des dépendances trie les résultats de la même manière et marque chacun avec son verdict lorsqu’il en existe un pour le commit de la pull request : reachable avec la fonction ou le fichier et la ligne, imported avec le fichier et la ligne (npm, Python, Java et Kotlin), ou no call path. Avec la politique de ne pas bloquer sur les résultats non atteignables activée, il liste aussi les résultats qui n’ont pas bloqué et pourquoi.
Mesurer la couverture
La part de vos résultats de dépendances ouverts qui ont un verdict, et la part qui est non atteignable, par écosystème et sur 30 ou 90 jours, figurent sur la page des indicateurs de remédiation.
Ce que l’analyse statique ne peut pas voir
L’absence de chemin d’appel est une preuve solide, pas une certitude. Aucun des deux moteurs ne voit le code atteint par la réflexion, les plugins, unsafe, eval, l’injection de dépendances ou le code généré, ni le code en dehors de ce qui a été analysé : une CLI séparée, une bibliothèque partagée, ou pour Go tout ce qui est hors du binaire. Un avis peut aussi nommer les mauvaises fonctions.
Pour le mode source Go, le graphe d’appels couvre vos paquets main et vos fonctions exportées tels qu’ils sont compilés pour Linux sur amd64 sans cgo. Les appels via des valeurs d’interface et des valeurs de fonction sont suivis. Les appels par réflexion, et les appels présents uniquement dans des fichiers pour d’autres plateformes ou dans des fichiers cgo, ne le sont pas : c’est pourquoi ces résultats sont unknown (reflection, build_constraints) au lieu de non atteignable.
Pour JavaScript et TypeScript, le graphe d’appels est approximatif. Jelly est un analyseur issu de la recherche, et un require ou un import() dynamique empêche tout verdict unreachable pour le dépôt. Un verdict Importé dit que le paquet est chargé. Il ne dit pas que le code vulnérable s’exécute.
Pour Python ainsi que pour Java et Kotlin, les imports via d’autres paquets sont suivis (voir ci-dessus). Pour Python, cela ne fonctionne que dans la mesure où leur code nomme un module littéralement. Pour Java et Kotlin, cela ne fonctionne que dans la mesure où leurs classes compilées nomment une classe. Un paquet qui charge des modules ou des classes par des noms calculés, ServiceLoader ou la réflexion n’est pas suivi au-delà de ce point. Les frameworks qui chargent des classes à partir de la configuration, comme les pilotes JDBC et l’injection de dépendances, ne sont pas vus non plus. Un paquet Java ou Kotlin utilisé uniquement de cette façon apparaît comme unknown alors qu’il est chargé.
C’est pourquoi le groupe est dit probablement non exploitable, et pourquoi chaque verdict affiche le moteur, sa version et la date de la base : vérifiez les preuves avant d’ignorer un résultat.
API
Chaque résultat de GET /orgs/{org}/code-security/deps/findings et de GET /orgs/{org}/code-security/images/findings porte un objet reachability. Pour un binaire Go :
"reachability": {
"verdict": "reachable",
"level": "function",
"reason_unknown": null,
"engine": "govulncheck",
"engine_version": "v1.8.0",
"db_version": "2026-10-10T11:47:18Z",
"advisory_id": "GO-2026-5320",
"evidence": {
"binary": "usr/local/bin/app",
"frames": [{ "module": "github.com/yuin/goldmark", "package": "github.com/yuin/goldmark/renderer/html", "receiver": "Renderer", "function": "renderLink" }]
},
"computed_at": "2026-10-12T05:40:00.000Z"
}Pour le mode source Go, les frames vont du point d’entrée de votre dépôt jusqu’à la fonction concernée, en passant par la bibliothèque standard et votre propre handler (un test planté, 2026-10-13) :
"reachability": {
"verdict": "reachable",
"level": "function",
"reason_unknown": null,
"engine": "govulncheck",
"engine_version": "v1.8.0 (source, go1.27.2)",
"db_version": "2026-10-10T11:47:18.507452919Z",
"advisory_id": "GO-2026-6278",
"evidence": {
"mode": "source",
"manifest": "reach-go/a/go.mod",
"module": "github.com/gorilla/websocket",
"version": "v1.5.0",
"symbols": ["github.com/gorilla/websocket.*Upgrader.Upgrade"],
"frames": [
{ "file": "reach-go/a/main.go", "line": 22, "go_package": "example.com/a", "function": "main" },
{ "file": "src/net/http/server.go", "line": 3604, "package": "stdlib@v1.27.2", "go_package": "net/http", "receiver": "*Server", "function": "Serve" },
{ "file": "src/net/http/server.go", "line": 2358, "package": "stdlib@v1.27.2", "go_package": "net/http", "receiver": "HandlerFunc", "function": "ServeHTTP" },
{ "file": "reach-go/a/main.go", "line": 13, "go_package": "example.com/a", "function": "handler" },
{ "file": "server.go", "line": 125, "package": "github.com/gorilla/websocket@v1.5.0", "go_package": "github.com/gorilla/websocket", "receiver": "*Upgrader", "function": "Upgrade" }
]
},
"computed_at": "2026-10-13T19:43:52.589Z"
}Une frame sans package se trouve dans votre dépôt, avec un file relatif au dépôt. Une frame avec package se trouve dans une dépendance ou dans la bibliothèque standard, et son file est relatif à ce module. Au plus 8 frames sont conservées : le point d’entrée, puis les 7 dernières jusqu’à la fonction concernée (l’exemple ci-dessus omet 3 frames de la bibliothèque standard).
Pour un paquet npm que votre code charge via un autre paquet :
"reachability": {
"verdict": "reachable",
"level": "import",
"reason_unknown": null,
"engine": "jelly",
"engine_version": "0.13.0",
"db_version": "2026-10-12T02:00:00.000Z",
"advisory_id": "GHSA-pxg6-pf52-xh8x",
"evidence": {
"manifest": "package-lock.json",
"package": "cookie",
"version": "0.6.0",
"frames": [
{ "file": "src/server.ts", "line": 3 },
{ "file": "index.js", "line": 9, "package": "express@4.21.0" },
{ "file": "index.js", "line": 1, "package": "cookie@0.6.0" }
]
},
"computed_at": "2026-10-12T09:15:00.000Z"
}Pour un paquet Python que votre code importe :
"reachability": {
"verdict": "reachable",
"level": "import",
"reason_unknown": null,
"engine": "python-imports",
"engine_version": "1.1.0 (python 3.13.5)",
"db_version": "2026-10-12T02:00:00.000Z",
"advisory_id": "GHSA-9hjg-9r4m-mvj7",
"evidence": {
"manifest": "requirements.txt",
"package": "requests",
"version": "2.31.0",
"mapping": "wheel requests-2.31.0-py3-none-any.whl",
"provides": ["requests"],
"frames": [{ "file": "app/main.py", "line": 2 }]
},
"computed_at": "2026-10-12T15:00:00.000Z"
}Pour un paquet Python chargé uniquement via un autre paquet, frames est le chemin, comme pour npm, et via nomme les paquets qui le composent :
"evidence": {
"manifest": "requirements.txt",
"package": "idna",
"version": "3.6",
"mapping": "wheel idna-3.6-py3-none-any.whl",
"provides": ["idna"],
"via": ["requests@2.32.3"],
"frames": [
{ "file": "app/main.py", "line": 2 },
{ "file": "requests/models.py", "line": 401, "package": "requests@2.32.3" }
]
}Les verdicts Maven ont la même forme, avec engine valant jvm-imports, mapping nommant le JAR et provides listant ses paquets Java. Via un autre paquet, une frame dans un JAR nomme le fichier source issu des informations de débogage de la classe et la ligne où la classe du paquet suivant est utilisée. La ligne vaut 0 lorsqu’aucune instruction ne l’utilise, comme pour une interface implémentée :
"evidence": {
"manifest": "pom.xml",
"package": "org.apache.commons:commons-text",
"version": "1.9",
"mapping": "jar commons-text-1.9.jar",
"via": ["org.apache.commons:commons-configuration2@2.7"],
"frames": [
{ "file": "src/main/java/app/App.java", "line": 3 },
{ "file": "org/apache/commons/configuration2/PropertiesConfiguration.java", "line": 1663, "package": "org.apache.commons:commons-configuration2@2.7" }
]
}Pour une release Python uniquement en sdist, mapping nomme le sdist et la manière dont les noms ont été trouvés, par exemple sdist pysaml2-4.4.0.tar.gz, package directories under src/.
La première frame npm est la ligne dans votre dépôt. Les frames suivantes sont des fichiers et des lignes dans les paquets sur le chemin, chacun nommé avec sa version.
Un résultat de pull request porte aussi block_waived : "unreachable" lorsque la politique de ne pas bloquer sur les résultats non atteignables l’a laissé passer fremforge/dep-scan lors de sa dernière analyse, sinon null. Ce n’est pas une dérogation.
La politique elle-même se lit dans dep_scan_unreachable_policy sur GET /orgs/{org}/security et se règle avec PUT /orgs/{org}/security/dep-scan-unreachable-policy (portée policy:write) :
{ "enabled": true, "repos": ["web", "acme/api"] }enabled est obligatoire. repos accepte des noms de dépôts de votre organisation, seuls ou sous la forme org/name ; omettez-le, ou envoyez [], pour tous les dépôts. La réponse contient la politique enregistrée, la précédente et si elle a changé.
Ajoutez ?reachability=reachable, unreachable ou unknown pour ne lister que ceux-là. Les trois valeurs partitionnent la liste. Sans le paramètre, tous les résultats sont renvoyés. Les preuves ne contiennent que des emplacements de code, jamais de contenu de code.
Ajoutez ?sort=risk pour lister les résultats dans l’ordre de tri : verdict (atteignable, puis inconnu ou non analysé, puis non atteignable), puis gravité, puis exploitation connue (résultats de dépendances et d’images), puis EPSS pour les résultats de dépendances, puis les plus anciens d’abord. La pagination avec offset est stable dans cet ordre. Sans ce paramètre, ou avec sort=created, les résultats sont listés du plus récent au plus ancien, comme avant.
Chaque résultat SAST de GET /orgs/{org}/code-security/sast/findings porte ses signaux de tri :
"triage": {
"confidence": "lower",
"reasons": ["test"],
"location": "test",
"dataflow": true,
"dismissed_before": { "repo": 0, "org": 1, "days": 90 }
}dataflow vaut true pour une correspondance taint, false pour une correspondance par motif seul et null pour un résultat relevé avant l’existence du signal. L’analyse suivante du dépôt le renseigne.