Naar de hoofdinhoud

Bereikbaarheid

Een advisory voor een afhankelijkheid zegt dat een versie van een package kwetsbaar is. Het zegt niet of je programma de kwetsbare code kan bereiken. Bereikbaarheid beantwoordt dat voor Go, op basis van de binaries die je echt uitlevert, en op basis van de code in je repository voor JavaScript en TypeScript (npm) en, op importniveau, Python (PyPI) en Java en Kotlin (Maven). Het laat je zien waarom. Voor Go analyseert het ook de eigen code van je repository (bronmodus); zie hieronder.

Wat er wordt gecontroleerd

Go: de binaries die je uitlevert

Wanneer fremforge de containerimages in de registry van je organisatie opnieuw scant, wordt elke Go-binary met een bevinding voor een kwetsbaarheid gelezen door govulncheck in binaire modus. govulncheck leest de modulelijst en de symbooltabel van de binary en vergelijkt die met de Go-kwetsbaarhedendatabase, die per advisory de precieze getroffen functies noemt.

  • Niets in je image wordt gebouwd of uitgevoerd. De binary wordt als bestand gelezen.
  • Er wordt geen dienst aangeroepen. De kwetsbaarhedendatabase is fremforges eigen kopie van de OSV-records voor Go, die dagelijks wordt vernieuwd.
  • govulncheck (BSD-3-Clause) is gepind op een release en wordt bijgewerkt via Renovate.

Terraform- en OpenTofu-providers: de binaries die je locks pinnen

Een bevinding in een Terraform- of OpenTofu-provider is een Go-module die is ingebouwd in de providerbinary die je .terraform.lock.hcl pint. fremforge leest die binary met govulncheck in binaire modus, net als een Go-binary in een image, en de bevinding krijgt het oordeel: bereikbaar als een getroffen functie in de provider is gelinkt, niet bereikbaar als de module (of het package ervan) in de binary zit maar geen van de functies die de advisory noemt.

  • Providerreleases worden gebouwd zonder symbooltabel (-ldflags=-s). De functietabel die Go tijdens het draaien bijhoudt, is er nog wel; fremforge herstelt het ene symbool dat govulncheck nodig heeft om die te lezen, en het resultaat is hetzelfde als voor dezelfde provider gebouwd met zijn symbolen. Als dat symbool niet te vinden is, blijft het oordeel unknown (no_symbol_table).
  • Elke providerversie wordt één keer geanalyseerd, voor elke organisatie die haar pint, uit de kopie die de providermirror van fremforge bewaart. Er wordt niets over je repository gebruikt.
  • Een providerbinary van meer dan 512 MiB (bijvoorbeeld hashicorp/aws) houdt het oordeel unknown (engine_failed).

Go: de code van je repository

Wanneer een scan van afhankelijkheden kwetsbare Go-modules vindt, draait fremforge govulncheck ook in bronmodus op de gescande commit. Het controleert de types van je modules en bouwt een aanroepgraaf vanaf je main-packages, zodat het het aanroeppad kan tonen van een regel in je repository naar de getroffen functie, ook als er van die commit geen image is gebouwd.

Daarvoor is het go-commando nodig, dat een repository kan proberen te sturen. fremforge draait het zo dat niets uit je repository wordt uitgevoerd en niets wordt opgehaald behalve via de dependency proxy van je organisatie. Het ontwerp is beoordeeld voordat het werd uitgebracht.

  • Niets in je repository draait. Geen go generate, geen tests, geen build, geen cgo (CGO_ENABLED=0), geen -toolexec. Alleen de eigen typechecker van de Go-toolchain leest je code.
  • Geen andere toolchain. Een toolchain-regel wordt genegeerd (GOTOOLCHAIN=local). Een go-regel die nieuwer is dan de Go van fremforge maakt de module unknown (build_failed). fremforge houdt zijn Go actueel via Renovate.
  • Geen versiebeheer en geen ander netwerk. Modules komen alleen uit de dependency proxy van je organisatie, via een relay die de scaninloggegevens van fremforge toevoegt, zodat je packagebeleid geldt. Er is geen terugval op een host voor versiebeheer. Als je organisatie haar dependency proxy heeft uitgezet, leest de scan modules in plaats daarvan uit de eigen scancache van fremforge op dezelfde proxy. Die cache bevat alleen openbare modules van proxy.golang.org, elk gecontroleerd tegen sum.golang.org, en gebruikt geen inloggegevens van jou, dus je packagebeleid geldt niet voor die leesacties. Een organisatie waarvan de proxy een module op grond van haar beleid weigert, krijgt daarvoor unknown; de scan gaat nooit om je proxy heen.
  • Privémodules worden nooit opgevraagd. Een module onder frem.sh, of een die overeenkomt met een GOPRIVATE-, GONOPROXY- of GONOSUMDB-patroon dat je repository declareert (in de CI-workflows, go.env, .envrc, .env of Makefile), wordt geweigerd voordat er een verzoek de scan verlaat. Het pad ervan wordt nooit naar de dependency proxy, naar proxy.golang.org of naar sum.golang.org gestuurd, en alles wat de module nodig heeft, is unknown (build_failed). Dezelfde regel geldt voor de Go-opzoekingen van licenties, releaseleeftijd en afhankelijkheidsgraaf. Zie wat Google bereikt voor openbare modules.
  • Je instellingenbestanden worden niet gelezen. Een ingecheckt GOFLAGS-bestand, go.env of .config/go/env, een map vendor/ en een .netrc worden genegeerd. Symlinks worden niet gevolgd.
  • Geweigerd vóór de analyse, als unknown (build_failed): een module die onder een go.work valt (die bepaalt welke code wordt gebouwd), een replace naar een absoluut pad of naar buiten de repository, en een modulepad dat rechtstreeks een repository voor versiebeheer noemt (….git).
  • Gescheiden van de inloggegevens van fremforge. De analyse draait in een eigen container die geen inloggegevens bevat en niets bereikt behalve de relay. Ze heeft eigen limieten voor geheugen en CPU.
  • Limieten: 20 modules, 20.000 bestanden en 128 MiB van je repository per scan, 512 MiB aan moduledownloads, en een tijdsbudget van 3 minuten per scan. Boven een limiet is het unknown.
  • Tests (_test.go) worden niet geanalyseerd, net als in binaire modus. De standaardbibliotheek wordt alleen in binaire modus beoordeeld: bronmodus zou haar lezen in de Go-versie van fremforge, niet in die van jou.
  • Het laat de scan nooit falen en verandert nooit de status fremforge/dep-scan.

JavaScript en TypeScript: de code van je repository

Wanneer een scan van afhankelijkheden kwetsbare npm-packages vindt, draait fremforge Jelly, een statische analyse van aanroepgrafen voor JavaScript en TypeScript van de Universiteit van Aarhus, op de gescande commit. Jelly controleert of je code het kwetsbare package laadt, direct of via andere packages.

  • Jelly leest je code. Het voert die nooit uit: er wordt niets geïnstalleerd, er draait geen packagecode en er draait geen installatiescript.

  • De packages op de weg naar een kwetsbaar package worden op naam en versie opgehaald en als data uitgepakt. Elk wordt gecontroleerd tegen de integriteitshash in je lockfile, en de .npmrc van je repository wordt niet gebruikt. Ze komen via de dependency proxy van je organisatie (zie Waar packagebestanden vandaan komen).

  • Elke tarball wordt als stream gelezen. Hij wordt tijdens het binnenkomen tegen zijn integriteitshash gecontroleerd, en alleen de bestanden die Jelly leest worden weggeschreven: .js, .mjs, .cjs, .jsx, .ts, .mts, .cts, .tsx en .json. Native binaries en andere grote bestanden worden overgeslagen zonder te worden bewaard, dus een package als onnxruntime-node (108 MiB) wordt geanalyseerd zoals elk ander. Bestanden worden alleen bewaard als de hash klopt.

  • Limieten: een tarball van maximaal 512 MiB, 2 GiB na uitpakken, 100.000 items, 10.000 weggeschreven bestanden en 16 MiB per weggeschreven bestand. Alle packages die voor één lockfile worden klaargezet, mogen samen tot 96 MiB aan weggeschreven bestanden innemen. Een package boven een limiet maakt de bevindingen die het nodig hebben unknown (build_failed). Een bestand dat om zijn grootte is overgeslagen, laat een bevinding nooit unreachable worden.

  • Ondersteunde lockfiles:

    LockfileVersiesIntegriteit gecontroleerd tegen
    package-lock.json, npm-shrinkwrap.jsonlockfileversie 2 of 3 (npm 7 en later)de lockfile
    pnpm-lock.yaml5.x, 6 en 9de lockfile
    yarn.lock, classicyarn 1de lockfile (integrity, of de SHA-1 in een oudere resolved-URL)
    yarn.lock, berryyarn 2 en later (__metadata-versie 4 en later)de integriteit die je registry voor die versie publiceert, omdat de checksum van berry yarns eigen cachearchief dekt en niet de npm-tarball. Een package waarvan de gepubliceerde integriteit niet kan worden gelezen, wordt niet opgehaald.
    bun.locktekstformaat (bun 1.2 en later)de lockfile

    Voor yarn classic, dat geen workspaces vastlegt, worden ook de package.json naast de lockfile en de workspaces daarin gelezen. bun.lockb, het oudere binaire formaat van bun, wordt niet gelezen: het is een ongedocumenteerde geheugenafbeelding die tussen bun-versies veranderde. De bevindingen ervan zijn unknown, en het bewijs zegt waarom. Andere lockfiles geven unknown.

  • Jelly (BSD-3-Clause) draait binnen de scanjob van fremforge en roept geen dienst aan. Het is gepind op een release en wordt bijgewerkt via Renovate.

  • De analyse heeft een tijdsbudget van 2 minuten per scan. Ze laat de scan nooit falen. Ze verandert de status fremforge/dep-scan alleen als je organisatie ervoor kiest niet te blokkeren op niet-bereikbare bevindingen.

Python, Java en Kotlin: de imports van je repository

Wanneer een scan van afhankelijkheden kwetsbare PyPI- of Maven-packages vindt, controleert fremforge of je code ze importeert. Dit is alleen importniveau: het vertelt je dat een package wordt geladen, nooit dat de kwetsbare code draait, en het geeft nooit unreachable.

  • Python. Je .py-bestanden worden geparset met Pythons eigen module ast, die alleen parset. Er wordt niets geïmporteerd of uitgevoerd. fremforge leest import- en from … import-statements, en aanroepen van importlib.import_module(...) en __import__(...) met een letterlijke naam. Een bestand dat niet parset met de huidige Python, zoals Python 2-code, wordt in plaats daarvan regel voor regel gelezen op zijn importstatements. Relatieve imports (from . import x) zijn je eigen modules en worden overgeslagen, net als virtuele omgevingen (.venv, venv, site-packages) en symlinks.

  • Python, via andere packages. Als je code een kwetsbaar package niet zelf importeert, volgt fremforge de packages die je code wel importeert: het leest hun code op dezelfde manier uit hun wheels, te beginnen bij de modules die je code importeert, en volgt alleen hun eigen imports. Als code die zo wordt bereikt het kwetsbare package importeert, is de bevinding Geïmporteerd, en het bewijs toont het pad: jouw import, daarna de import binnen elk package, bijvoorbeeld idna via requests. Een module die door niets wordt geladen (zoals de help.py van een package) en imports onder if TYPE_CHECKING: worden niet gevolgd. Deze tocht gaat maximaal drie packages diep en leest per scan maximaal 40 wheels van elk tot 16 MiB. Hij voegt alleen oordelen Geïmporteerd toe; een pad dat hij niet kan volgen, laat de bevinding unknown.

  • Java, Kotlin, Groovy en Scala. fremforge leest import-statements, volledig gekwalificeerde klassenamen in code, en Class.forName("…") en loadClass("…") met een letterlijke naam. Commentaar en stringliterals worden genegeerd, net als build/ en target/. Er wordt niets gecompileerd.

  • Java en Kotlin, via andere packages. Als je code een kwetsbaar Maven-package niet zelf importeert, volgt fremforge de packages die je code wel importeert, in hun gecompileerde klassen. Het begint bij de klassen die je code importeert (elke klasse van het package bij een wildcard import a.b.*). Van daaruit volgt het de klassen die zij gebruiken: elke klasse waarvan een methode wordt aangeroepen, die wordt aangemaakt, waarnaar wordt gecast of waarvan een veld wordt gelezen, en elke superklasse en interface. Die worden uit de constant pool van elk classbestand gelezen. Als een klasse die zo wordt bereikt een klasse van het kwetsbare package gebruikt, is de bevinding Geïmporteerd. Het bewijs toont het pad: jouw import, daarna het bronbestand en de regel binnen elk package waar het volgende wordt gebruikt. Een voorbeeld is commons-text via commons-configuration2. Een klasse die door niets wordt bereikt, wordt niet gevolgd, en een klasse die op naam uit een string wordt geladen evenmin. Classbestanden worden als data gelezen: er wordt geen klasse geladen of uitgevoerd, en er komt geen JVM aan te pas. Deze tocht gaat maximaal drie packages diep en leest per scan maximaal 40 JAR’s van elk tot 32 MiB, één tegelijk. Hij voegt alleen oordelen Geïmporteerd toe; een pad dat hij niet kan volgen, laat de bevinding unknown.

  • Welke namen een package levert, komt uit de bestandslijst van zijn gepubliceerde archief, gelezen als data:

    • PyPI: de wheel voor die versie. Een namespace-package wordt herleid tot zijn eerste echte package, bijvoorbeeld google.protobuf voor protobuf.
    • PyPI, een release zonder wheel (alleen sdist): het bronarchief, gelezen als data en nooit geïnstalleerd. setup.py wordt niet uitgevoerd en niet gelezen. De namen komen uit de packagelijsten in setup.cfg of pyproject.toml (setuptools, hatch, flit en poetry), of anders uit de mappen met een __init__.py, op het hoogste niveau, onder src/ of onder een map zoals lib/. Zo levert pysaml2 saml2 en levert pycrypto Crypto. PKG-INFO moet hetzelfde project noemen. Archieven tot 16 MiB worden gelezen (.tar.gz, .tar, .zip). Een release die op geen enkele index staat, of een sdist die niets noemt (bijvoorbeeld een .tar.bz2), valt terug op de projectnaam, zodat requests staat voor import requests; de mapping van het bewijs zegt dat.
    • Maven: de JAR op Maven Central, en de Java-packages waarin zijn klassen staan.

    Voor een wheel of JAR wordt alleen de inhoudsopgave van het archief opgehaald, met HTTP-range-requests, dus een grote kost een paar kilobyte. De tocht via andere packages leest de hele wheel of JAR van elk package dat hij volgt, en een sdist wordt altijd helemaal gelezen. Er wordt niets geïnstalleerd, gebouwd of uitgevoerd.

  • Je repository wordt niet gebouwd. Java-bereikbaarheid met aanroepgrafen, zoals de experimentele JAR-analyse van osv-scanner of jdeps, heeft een gebouwde JAR nodig. Een scan van een pull request bouwt je project niet, dus die tools worden niet gebruikt.

  • De analyse heeft een tijdsbudget van 1 minuut per ecosysteem per scan en draait naast de npm-analyse. Ze laat de scan nooit falen en verandert nooit de status fremforge/dep-scan.

Waar packagebestanden vandaan komen

npm-tarballs, PyPI-indexen en -wheels, en Maven-JAR’s worden gelezen via de dependency proxy van je organisatie, over een interne route die alleen de scandienst van fremforge kan bereiken. Het packagebeleid, de cache, de opslaglimiet en de upstreamsnelheid van je organisatie gelden er dus voor zoals voor elke andere pull, en wat de scan ophaalt, wordt gecachet voor je builds.

  • Als je beleid een package weigert (bijvoorbeeld malware, een te nieuwe release of een licentieregel), haalt de scan het nergens anders op. De bevindingen die het nodig hebben, zijn unknown (build_failed), en het bewijs noemt de beslissing van de proxy.
  • Als je organisatie de dependency proxy heeft uitgezet, of de proxy niet kan antwoorden, worden de bestanden zoals voorheen uit de openbare registries gelezen (npmjs, PyPI, Maven Central).
  • Bestanden die de scan ophaalt, tellen mee voor de proxyopslag van je organisatie, zoals elk ander gecachet bestand.

De niveaus

Een oordeel heeft een niveau: hoeveel de engine kon zien.

NiveauBetekenis
functionDe advisory noemt de getroffen functies, en de engine heeft naar een pad daarheen gezocht. Dit is wat govulncheck voor Go geeft.
importDe advisory noemt geen getroffen functies. De engine kan alleen zeggen of het kwetsbare package wordt geladen.

npm-advisories noemen bijna nooit getroffen functies. Toen dit op 2026-10-12 werd gemeten, deed geen van de 230.287 npm-records in de OSV-database dat. Oordelen voor JavaScript en TypeScript zijn dus importniveau: ze vertellen je dat je code het kwetsbare package laadt, niet dat ze de kwetsbare code aanroept. Als een npm-advisory wel getroffen functies noemt, controleert Jelly die op functieniveau.

Oordelen voor Python en Maven zijn altijd importniveau. Dezelfde meting vond gegevens over getroffen functies in 2 van de 26.221 PyPI-records en in geen van de 7.190 Maven-records.

De drie oordelen

Elke bevinding in afhankelijkheden en images heeft een van deze:

OordeelGo (govulncheck)JavaScript en TypeScript (Jelly)Python, Java en Kotlin (importniveau)
BereikbaarEen getroffen functie is in de binary gelinkt. De Go-linker laat code weg die niets kan aanroepen, dus een gelinkte functie is er een die je programma kan bereiken. De markering noemt de functie, bijvoorbeeld html.Renderer.renderLink. In bronmodus: een aanroeppad vanuit je code bereikt een getroffen functie; de markering geeft het bestand en de regel van de laatste aanroep in je repository voordat het pad de afhankelijkheid binnengaat, bijvoorbeeld je handler in plaats van main. Het hele pad staat in het bewijs.Importniveau, getoond als Geïmporteerd: je code laadt het package in de kwetsbare versie, direct of via andere packages. Functieniveau, getoond als Bereikbaar: een aanroep in je code kan een getroffen functie bereiken. De markering geeft het bestand en de regel in je repository, bijvoorbeeld src/session.ts:12.Getoond als Geïmporteerd: je code importeert een module of Java-package die het kwetsbare package levert. De markering geeft het bestand en de regel van de import, bijvoorbeeld app/main.py:2.
Niet bereikbaar (unreachable)De module zit in een getroffen versie in de binary, de advisory noemt de getroffen functies, govulncheck is volledig doorgelopen, en geen van die functies zit in de binary. In bronmodus: de module wordt in een getroffen versie gebruikt, govulncheck vond geen aanroeppad naar een getroffen functie, en er staat niets in je code dat govulncheck niet kan volgen (reflectie, bestanden voor andere platforms, cgo, plug-ins).De advisory noemt de getroffen functies, Jelly is volledig doorgelopen met elk package onderweg beschikbaar, en geen aanroep in je code bereikt er een. Zonder die functiegegevens is een package dat je code nooit laadt unknown, niet unreachable.Nooit. Imports binnen andere packages worden alleen gevolgd voor zover ze een module of klasse letterlijk noemen, dus een package dat je code niet lijkt te laden, kan toch worden geladen.
OnbekendAl het andere. Dit is de standaard, en het wordt nooit als veilig behandeld.Hetzelfde.Hetzelfde, inclusief elk package dat je code niet importeert.

unreachable vereist al zijn voorwaarden. Een ontbrekende invoer geeft altijd unknown, met de reden:

RedenWaarom
no_symbol_dataDe advisory noemt de getroffen functies niet, dus de afwezigheid van een pad bewijst niets. Voor npm is dit de gebruikelijke reden als je code het package niet laadt. Voor Python en Maven is het de reden telkens als je code het package niet importeert.
no_symbol_tableGo: de binary is gebouwd met -ldflags=-s. De symbolen ervan zijn niet te lezen, dus geen van beide oordelen is verdiend. Bouw zonder -s (-w behouden mag) om oordelen te krijgen. Terraform-providers: het symbool dat fremforge herstelt, is in deze binary niet gevonden.
build_failedGo-bronmodus: de module is vóór de analyse geweigerd (zie hierboven), kon niet worden geladen, of een module kon niet uit je dependency proxy worden opgehaald. npm: de lockfile heeft geen van de ondersteunde formaten, of een package onderweg kon niet worden opgehaald, zat boven een limiet of kwam niet overeen met zijn integriteitshash. Python en Maven: de wheel, sdist of JAR kon niet worden gelezen, bijvoorbeeld een artefact met alleen een POM of een dat niet op Maven Central staat.
dynamic_codeJe code laadt modules of klassen met berekende namen, dus een ontbrekend aanroeppad of een ontbrekende import bewijst niets. npm: require(name) en import(name). Python: importlib.import_module(name) en __import__(name). Java: Class.forName(name) en loadClass(name). Go-bronmodus: het package plugin of //go:linkname.
reflectionGo-bronmodus: je code roept methoden aan via reflect (MethodByName, Method, Call), of gebruikt text/template of html/template terwijl de advisory methoden noemt. govulncheck kan die aanroepen niet volgen, dus een ontbrekend aanroeppad bewijst niets.
build_constraintsGo-bronmodus: een bestand dat de analyse niet heeft geladen, importeert andere packages. De analyse laadt je code als voor Linux op amd64 zonder cgo, dus een _windows.go-bestand, een bestand waarvan de //go:build-regel Linux uitsluit, of een cgo-bestand zit niet in de aanroepgraaf, en een aanroep die alleen daar gebeurt, zou worden gemist. Een package dat alleen met cgo bouwt (bijvoorbeeld een desktop-UI op een toolkit die alleen met cgo werkt), wordt buiten de analyse gelaten in plaats van haar te laten mislukken: de rest van de module wordt geanalyseerd, en elke module die dat package importeert, krijgt deze reden in plaats van unreachable.
timeoutDe analyse duurde langer dan haar tijdsbudget (5 minuten per image voor Go, 2 minuten per scan voor npm, 1 minuut per ecosysteem per scan voor Python en Maven).
engine_no_matchGo: govulncheck meldde deze moduleversie niet als getroffen door de advisory, hoewel de imagescanner dat wel deed. Geen van beide wordt boven de ander vertrouwd.
oom_killedGo-bronmodus: de analyse had te weinig geheugen en werd gestopt, dus er is niets beslist.
engine_failedDe analyse kon niet draaien op deze binary of repository.
not_analysedEr is geen oordeel berekend: de bevinding is niet meer gescand sinds bereikbaarheid werd toegevoegd, of voor een Go-bevinding is er geen image van haar commit gebouwd.
unsupported_ecosystemEen bevinding in een afhankelijkheid buiten Go, npm, PyPI, Maven en Terraform-providers, bijvoorbeeld NuGet of Cargo.

Bevindingen in afhankelijkheden

Go. Een Go-bevinding in een afhankelijkheid krijgt haar oordeel in binaire modus, van een image dat is gebouwd van precies die commit. Waar bronmodus aanstaat (tot nu toe in de interne organisaties van fremforge), krijgt ze er ook bij elke scan een, met het aanroeppad in je repository. Label je images met de twee standaard-OCI-labels:

org.opencontainers.image.source=https://frem.sh/<org>/<repo>
org.opencontainers.image.revision=<full commit sha>

docker/metadata-action zet ze allebei. Het oordeel geldt alleen voor de commit die het image noemt en voor de moduleversies waarmee de binaries zijn gebouwd. Als een Go-binary in dat image niet kon worden gelezen, blijft de bevinding in de afhankelijkheid unknown. Als meerdere binaries de module bevatten, is de bevinding bereikbaar zodra een van hen haar bereikt.

Als beide modi dezelfde bevinding op dezelfde commit beoordelen, wist geen van beide de ander uit: een unknown uit de ene vervangt nooit een oordeel uit de andere, en niet bereikbaar uit de ene vervangt nooit bereikbaar uit de andere. Bewijs uit bronmodus bevat "mode": "source".

npm, PyPI en Maven. Oordelen komen met de scan van afhankelijkheden zelf mee: elke scan van een pull request, en de scan van je standaardbranch na elke push (hooguit één keer per uur). Een oordeel hoort altijd bij de commit die is gescand. Als hetzelfde package en dezelfde advisory in meerdere lockfiles voorkomen, is de bevinding bereikbaar zodra een van die lockfiles haar bereikt.

Hoe fremforge het gebruikt

Rangschikking. De lijsten met bevindingen voor Afhankelijkheden en Containerimages staan in deze volgorde: eerst bereikbaar, dan onbekend, dan niet bereikbaar. Binnen elke groep staan ze op ernst, dan bekend misbruikt vóór de rest, dan op EPSS. Een bevinding is bekend misbruikt als een van haar CVE-id’s, de eigen of de aliassen van haar advisory, als misbruikt wordt vermeld door de EU Vulnerability Database van ENISA of door de Known Exploited Vulnerabilities-catalogus van CISA. Beide lijsten tonen een badge Bekend misbruikt waarvan de tooltip de bron noemt: EU (ENISA EUVD), VS (CISA KEV) of allebei, en boven elke lijst toont Alleen bekend misbruikt alleen die bevindingen (de ?known_exploited=true van de Findings-API). Beide lijsten worden dagelijks vernieuwd. Een bevinding Geïmporteerd wordt gerangschikt bij de bereikbare. Dezelfde volgorde wordt gebruikt door:

  • het tabblad Aanbevelingen, waar een kaart (één upgrade van één package in één repository) het meest bereikbare oordeel krijgt van de bevindingen die ze oplost en gemarkeerd wordt als Bereikbaar of No call path;
  • Top fixable today in de wekelijkse digest, dat bij elke rij het oordeel toont;
  • de Findings-API met ?sort=risk.

Meldingen. Wanneer een openstaande bevinding in een afhankelijkheid bereikbaar wordt, of bekend misbruikt wordt, krijgt je SIEM direct een auditgebeurtenis en krijgen de beveiligingscontactpersonen van de organisatie een e-mail, één keer per bevinding: zie Meldingen: nieuw bereikbaar of bekend als misbruikt. Bekend misbruikt, en bereikbaar met ernst HIGH of CRITICAL, worden direct gemaild. Bereikbaar met ernst MEDIUM of LOW gaat naar het dagelijkse meldingsoverzicht van elke persoon, of direct voor iemand die het overzicht heeft uitgezet. De installatiechecklist onder Beveiliging › Overzicht toont welke van je images Go-binaries zonder symbolen hebben.

De groep waarschijnlijk niet uit te buiten. Bevindingen die niet bereikbaar zijn, gaan naar een ingeklapte groep onderaan de lijst, met hun aantal in de titel. Ze blijven zichtbaar, selecteerbaar en te negeren, en ze tellen mee in elk totaal.

Niets wordt verborgen of genegeerd. Een oordeel schrijft nooit een negering weg. Het verandert de SLA van een bevinding alleen als je organisatie kiest voor de SLA-vermenigvuldiger. Standaard leest de commitstatus (fremforge/dep-scan, fremforge/image-scan) geen oordelen: wat een merge of een pull blokkeert, is ongewijzigd. De enige uitzondering is het opt-in beleid om pull requests niet te blokkeren op niet-bereikbare bevindingen, dat alleen voor fremforge/dep-scan geldt.

SLA-vermenigvuldiger (opt-in)

Een eigenaar van de organisatie kan de SLA per ernst verlengen voor bevindingen die niet bereikbaar zijn, onder Beveiliging › Prioriteren › Hersteltermijn (SLA) (/<org>/_admin/security/prioritise/sla), of met unreachable_sla_multiplier op PUT /orgs/{org}/security/sla-policy. De waarde is een geheel getal van 1 tot 4. De standaard, 1, betekent uit.

  • Alleen unreachable-bevindingen worden verlengd. Bereikbare en onbekende bevindingen houden altijd de volledige SLA, net als een bevinding die niet is geanalyseerd.
  • Een bekend misbruikte bevinding (vermeld door de EU (ENISA EUVD) of de VS (CISA KEV), zie Rangschikking hierboven) wordt nooit verlengd, bij geen enkele ernst en ongeacht haar oordeel.
  • Een kritieke bevinding met een EPSS-score in de bovenste 10 % (percentiel 0,90 of hoger) wordt nooit verlengd, ongeacht haar oordeel.
  • De deadline wordt altijd geteld vanaf het moment dat de bevinding voor het eerst werd gezien. Als een latere scan een aanroeppad vindt, of niet kan beslissen, geldt meteen weer de volledige SLA, dus de bevinding kan direct over de SLA heen gaan. Het SLA-label zegt dan waarom.
  • Met de vermenigvuldiger aan toont een verlengd SLA-label een kleine ×2 (of ×3, ×4) en een tooltip met beide deadlines. Het aantal SLA-overschrijdingen in de wekelijkse digest, de tegel op het overzicht en de SIEM-gebeurtenis code-security.sla.breached gebruiken dezelfde verlengde deadlines.
  • De vermenigvuldiger verandert alleen deadlines. Hij verandert niet wat een pull request blokkeert, en hij verbergt niets.

Elke wijziging wordt geaudit als code-security.sla_policy.updated, met de vorige waarde.

Niet blokkeren op bevindingen die niet bereikbaar zijn (opt-in)

Standaard blokkeert elke bevinding in een afhankelijkheid op of boven je blokkeerdrempel de pull request, ongeacht haar oordeel. Een eigenaar van de organisatie kan ervoor kiezen dat een bevinding met niet bereikbaar fremforge/dep-scan niet blokkeert. Dit staat uit tenzij je het aanzet.

Het risico. Een ontbrekend aanroeppad is sterk bewijs, geen sluitend bewijs (zie Wat statische analyse niet kan zien). Als de analyse een pad mist via reflectie, een plug-in, een dynamische import, dependency injection of code buiten de repository, of als de advisory de verkeerde functies noemt, laat een onterecht niet bereikbaar een echte, bereikbare kwetsbaarheid door bij het mergen. Zet dit alleen aan waar je dat accepteert, en overweeg het te beperken tot de repository’s waar je de analyse vertrouwt.

Zet het aan onder Beveiliging › Prioriteren › Pull request-controles (/<org>/_admin/security/prioritise/pr-checks), of met PUT /orgs/{org}/security/dep-scan-unreachable-policy (zie API). Je kunt het beperken tot een lijst repository’s; een lege lijst betekent elke repository in de organisatie.

Wat het verandert, en wat het nooit verandert:

  • Alleen unreachable met bewijs komt in aanmerking. Het oordeel van de bevinding voor de headcommit van de pull request moet niet bereikbaar zijn, berekend door een engine met haar versie en de getroffen functies uit de advisory die ze heeft gecontroleerd. Op dit moment betekent dat Go en JavaScript en TypeScript. Voor Go geeft govulncheck het oordeel in binaire modus zodra een image dat van de headcommit van de pull request is gebouwd, is gescand; een oordeel dat na de scan binnenkomt, geldt vanaf de volgende scan van de pull request. Waar bronmodus aanstaat (tot nu toe in de interne organisaties van fremforge), geeft het er ook een bij de scan van de pull request zelf. Voor JavaScript en TypeScript geeft Jelly het als de advisory getroffen functies noemt. Oordelen voor Python, Java en Kotlin zijn importniveau en nooit unreachable, dus die blokkeren altijd.
  • unknown blokkeert altijd. Er is geen optie om niet te blokkeren op unknown, en een bevinding die niet is geanalyseerd, is unknown.
  • Malware en bekend misbruikte kwetsbaarheden blokkeren altijd, ongeacht het oordeel: een MAL--advisory of een advisory die als kwaadaardig is gemarkeerd, en een kwetsbaarheid waarvan een van de CVE-id’s als misbruikt wordt vermeld door de EU (ENISA EUVD) of de VS (CISA KEV). Als fremforge die feiten tijdens een scan niet kan lezen, wordt er bij die scan niets doorgelaten.
  • Niets wordt verborgen. De bevinding wordt opgeslagen, vermeld, in elk totaal meegeteld en gemeld zoals elke andere. Ze wordt niet genegeerd, en haar SLA is ongewijzigd (tenzij de SLA-vermenigvuldiger ook aanstaat).
  • Alleen pull requests. De scan van je standaardbranch wordt precies zoals voorheen vastgelegd.
  • Treffers op de denylist en de blokkeerdrempel zijn ongewijzigd. Een bevinding onder de drempel blokkeerde nooit, en een geweigerde component blokkeert nog steeds.

De blokkering komt terug. Als een latere scan van de pull request een aanroeppad vindt, of niet kan beslissen, blokkeert de bevinding weer. Een oordeel dat buiten een scan wordt berekend (govulncheck op een image), en het uitzetten of inperken van het beleid, worden binnen het uur opgepikt: fremforge controleert elke pull request die dankzij het beleid slaagde opnieuw en berekent de status, het commentaar en de auditgebeurtenis opnieuw. Hetzelfde gebeurt als een bevinding bekend misbruikt wordt.

Wat de pull request toont. De status fremforge/dep-scan zegt hoeveel bevindingen niet blokkeerden, bijvoorbeeld 2 finding(s); 1 at or above critical with no call path, not blocking (org policy). Het commentaar markeert elke zulke bevinding als not blocking en vermeldt ze onder Not blocking: no call path (org policy), met de engine, haar versie, de datum van de kwetsbaarhedendatabase en de functies die ze heeft gecontroleerd. Een bevinding zonder aanroeppad die toch blokkeert, staat onder Blocking although no call path was found, met de reden (malware of bekend misbruikt).

Audit. Elke wijziging van het beleid wordt geaudit als code-security.deps.unreachable_policy.updated, met de nieuwe en de vorige waarde, of er iets veranderde, en of de wijziging uit de console of de API kwam. Elke scan van een pull request terwijl het beleid geldt, legt in zijn gebeurtenis code-security.dep_scan.ingest elke bevinding vast die niet blokkeerde (unreachable_not_blocking, met de engine, de versie en de databasedatum waarop werd vertrouwd) en elke die toch blokkeerde (unreachable_blocked_anyway, met why). Een hercontrole bevat trigger: unreachable_policy_recheck en de reden. Beide bereiken je SIEM-endpoints.

Als een oordeel verandert: reachability_changed

Een negering of een uitzondering kan op een oordeel steunen. Als je een bevinding in een afhankelijkheid of image negeert waarvan het oordeel niet bereikbaar is, of er een uitzondering voor aanvraagt, legt fremforge het oordeel vast waarop werd vertrouwd: de engine, haar versie, de databasedatum en de commit of het image waarop het is berekend. Het negeerpaneel zegt dat.

Elk uur controleert fremforge die negeringen en uitzonderingen. Als een scan na de negering dezelfde bevinding reachable vindt, of niet kan beslissen (unknown):

  • wordt een uitzondering ingetrokken met de reden reachability_changed;
  • wordt een gewone negering ingetrokken: de bevinding gaat weer open, en een negering voor de hele repository die ermee was gemaakt, geldt niet meer voor latere scans;
  • meldt de bevinding zich opnieuw, via dezelfde beveiligingsmelding als een verlopen negering;
  • zijn beide auditgebeurtenissen (code-security.exception.revoked, of code-security.deps.dismissal_revoked en code-security.images.dismissal_revoked) met het oordeel waarop werd vertrouwd en het nieuwe, en bereiken ze je SIEM-endpoints. De bevindingsgebeurtenis zegt het ook: security.finding.exception_revoked, of voor een gewone negering security.finding.dismissal_revoked, beide met transition_reason reachability_changed.

Een bevinding die niet opnieuw is geanalyseerd, houdt haar negering: het ontbreken van een nieuwe scan is geen nieuw oordeel. Een negering die is gemaakt terwijl het oordeel niet unreachable was, legt niets vast en wordt door deze controle nooit ingetrokken, en negeringen van vóór deze functie worden niet gecontroleerd. Heb je een bevinding genegeerd om een reden die niet van bereikbaarheid afhangt, negeer haar dan na de intrekking opnieuw; het oordeel is dan niet meer unreachable, dus de nieuwe negering blijft staan.

Triagesignalen voor SAST

SAST-bevindingen hebben geen aanroepgraaf, dus drie deterministische signalen rangschikken ze in plaats daarvan. Er komt geen AI-model aan te pas.

SignaalWat het is
LocatieUit het bestandspad: testcode (test/, __tests__/, *.test.ts, *_test.go, test_*.py, *Test.java en dergelijke), voorbeelden en documentatie, code van derden (vendor/, third_party/, node_modules/) en gegenereerde code (*.pb.go, *.min.js, gen/, zz_generated* en dergelijke).
Taint-padDe taint-modusregels van OpenGrep volgen data van een bron, zoals gebruikersinvoer, naar de sink. Een match met een spoor wordt gemarkeerd als Taint-pad en staat hoger dan een match op alleen een patroon. Alleen het feit wordt opgeslagen, nooit het spoor.
Hier vaak afgewezenMensen hebben dezelfde regel in de afgelopen 90 dagen genegeerd bij minstens 2 andere bevindingen in deze repository, of 5 in de organisatie. Bots, automatisch oplossen en andere systeemactoren tellen niet mee.

Een bevinding in test-, voorbeeld-, externe of gegenereerde code, of een die gemarkeerd is als hier vaak afgewezen, gaat naar een ingeklapte groep Minder betrouwbaar onderaan de SAST-lijst, met haar aantal in de titel. Een taint-pad haalt een bevinding niet uit die groep: een taint-match in een testbestand staat nog steeds in een testbestand. Binnen elke groep staan bevindingen op ernst, dan taint-pad, dan leeftijd, de oudste eerst.

Net als de groep waarschijnlijk niet uit te buiten worden minder betrouwbare bevindingen nooit verborgen of genegeerd: ze houden hun volledige SLA, tellen overal mee en kunnen worden geselecteerd, hersteld en genegeerd zoals elke andere. De SAST-Findings-API geeft de signalen terug als een object triage bij elke bevinding, en ?sort=risk vermeldt bevindingen over alle pagina’s heen in deze volgorde.

Commentaar op de pull request. Het commentaar van de scan van afhankelijkheden ordent bevindingen op dezelfde manier en markeert elke bevinding met haar oordeel als er een is voor de commit van de pull request: reachable met de functie of het bestand en de regel, imported met het bestand en de regel (npm, Python, Java en Kotlin), of no call path. Met het beleid om niet te blokkeren op niet-bereikbare bevindingen aan vermeldt het ook welke bevindingen niet blokkeerden en waarom.

Dekking meten

Het aandeel van je openstaande bevindingen in afhankelijkheden met een oordeel, en het aandeel dat niet bereikbaar is, per ecosysteem en over 30 of 90 dagen, staan op de pagina herstelstatistieken.

Wat statische analyse niet kan zien

Een ontbrekend aanroeppad is sterk bewijs, geen sluitend bewijs. Geen van beide engines ziet code die wordt bereikt via reflectie, plug-ins, unsafe, eval, dependency injection of gegenereerde code, of code buiten wat is geanalyseerd: een aparte CLI, een gedeelde bibliotheek, of bij Go alles buiten de binary. Een advisory kan ook de verkeerde functies noemen.

Voor Go-bronmodus dekt de aanroepgraaf je main-packages en geëxporteerde functies zoals gebouwd voor Linux op amd64 zonder cgo. Aanroepen via interfacewaarden en functiewaarden worden gevolgd. Aanroepen via reflectie, en aanroepen die alleen in bestanden voor andere platforms of in cgo-bestanden staan, niet; daarom zijn die bevindingen unknown (reflection, build_constraints) in plaats van niet bereikbaar.

Voor JavaScript en TypeScript is de aanroepgraaf bij benadering. Jelly is een analyse-instrument van onderzoeksniveau, en een dynamische require of import() voorkomt elk unreachable-oordeel voor de repository. Een oordeel Geïmporteerd zegt dat het package wordt geladen. Het zegt niet dat de kwetsbare code draait.

Voor Python en voor Java en Kotlin worden imports via andere packages gevolgd (zie hierboven). Voor Python werkt dat alleen voor zover hun code een module letterlijk noemt. Voor Java en Kotlin werkt het alleen voor zover hun gecompileerde klassen een klasse noemen. Een package dat modules of klassen laadt met berekende namen, ServiceLoader of reflectie, wordt voorbij dat punt niet gevolgd. Frameworks die klassen uit configuratie laden, zoals JDBC-drivers en dependency injection, worden evenmin gezien. Een Java- of Kotlin-package dat alleen op die manier wordt gebruikt, verschijnt als unknown, ook al wordt het geladen.

Daarom heet de groep waarschijnlijk niet uit te buiten, en daarom toont elk oordeel de engine, haar versie en de databasedatum: controleer het bewijs voordat je een bevinding negeert.

API

Elke bevinding uit GET /orgs/{org}/code-security/deps/findings en GET /orgs/{org}/code-security/images/findings bevat een object reachability. Voor een Go-binary:

"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"
}

Voor Go-bronmodus lopen de frames van het startpunt van je repository naar de getroffen functie, via de standaardbibliotheek en je eigen handler (een opzettelijk geplaatste test, 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"
}

Een frame zonder package staat in je repository, met een file relatief aan de repository. Een frame met package staat in een afhankelijkheid of de standaardbibliotheek, en zijn file is relatief aan die module. Er worden maximaal 8 frames bewaard: het startpunt, dan de laatste 7 tot aan de getroffen functie (het voorbeeld hierboven laat 3 frames uit de standaardbibliotheek weg).

Voor een npm-package dat je code via een ander package laadt:

"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"
}

Voor een Python-package dat je code importeert:

"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"
}

Voor een Python-package dat alleen via een ander package wordt geladen, is frames het pad, net als bij npm, en noemt via de packages daarop:

"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" }
  ]
}

Maven-oordelen hebben dezelfde vorm, met engine jvm-imports, een mapping die de JAR noemt en provides met de Java-packages ervan. Via een ander package noemt een frame binnen een JAR het bronbestand uit de debuginformatie van de klasse en de regel waar de klasse van het volgende package wordt gebruikt. De regel is 0 als geen instructie haar gebruikt, zoals bij een geïmplementeerde interface:

"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" }
  ]
}

Voor een Python-release met alleen een sdist noemt mapping de sdist en hoe de namen zijn gevonden, bijvoorbeeld sdist pysaml2-4.4.0.tar.gz, package directories under src/.

Het eerste npm-frame is de regel in je repository. De frames daarna zijn bestanden en regels binnen de packages onderweg, elk met zijn versie.

Een bevinding uit een pull request bevat ook block_waived: "unreachable" als het beleid om niet te blokkeren op niet-bereikbare bevindingen haar bij haar laatste scan door fremforge/dep-scan heeft gelaten, anders null. Het is geen negering.

Het beleid zelf lees je in dep_scan_unreachable_policy op GET /orgs/{org}/security en stel je in met PUT /orgs/{org}/security/dep-scan-unreachable-policy (scope policy:write):

{ "enabled": true, "repos": ["web", "acme/api"] }

enabled is verplicht. repos neemt namen van repository’s van je organisatie, kaal of als org/name; laat het weg, of stuur [], voor elke repository. Het antwoord bevat het opgeslagen beleid, het vorige en of het veranderde.

Voeg ?reachability=reachable, unreachable of unknown toe om alleen die te tonen. De drie waarden verdelen de lijst zonder overlap. Zonder de parameter wordt elke bevinding teruggegeven. Bewijs bevat alleen codelocaties, nooit code-inhoud.

Voeg ?sort=risk toe om bevindingen in triagevolgorde te tonen: oordeel (bereikbaar, dan onbekend of niet geanalyseerd, dan niet bereikbaar), dan ernst, dan bekend misbruikt (bevindingen in afhankelijkheden en images), dan EPSS voor bevindingen in afhankelijkheden, dan de oudste eerst. Pagineren met offset is stabiel in deze volgorde. Zonder de parameter, of met sort=created, worden bevindingen zoals voorheen getoond, de nieuwste eerst.

Elke SAST-bevinding uit GET /orgs/{org}/code-security/sast/findings bevat haar triagesignalen:

"triage": {
  "confidence": "lower",
  "reasons": ["test"],
  "location": "test",
  "dataflow": true,
  "dismissed_before": { "repo": 0, "org": 1, "days": 90 }
}

dataflow is true voor een taint-match, false voor een match op alleen een patroon en null voor een bevinding van vóór het signaal bestond. De volgende scan van de repository vult het in.