← Blog

Pendant des semaines, notre framework d'evaluation affichait fierement : FPR 0% (0/98). Aucun faux positif sur 98 packages npm populaires. On etait satisfaits.

On avait tort.

La decouverte embarrassante

En examinant le code de evaluateBenign(), une realite genante est apparue. La fonction creait des repertoires temporaires vides contenant uniquement un package.json avec le nom du package, puis lancait le scanner sur ces repertoires vides.

Les 14 scanners (AST, dataflow, obfuscation, entropie, etc.) n'avaient litteralement rien a analyser. Le 0% ne mesurait que le matching IOC et la detection de typosquatting sur les noms. Pas du tout la capacite du scanner a eviter les faux positifs sur du vrai code source.

Pendant ce temps, le TPR (ground truth) et l'ADR (adversarial) scannaient de vrais fichiers JavaScript. L'asymetrie etait flagrante : on testait les vrais positifs sur du vrai code, et les faux positifs sur du vide.

Ne jamais faire confiance a un FPR de 0%. Un scanner de securite qui ne genere aucun faux positif ne scanne probablement rien.

La correction : scanner du vrai code

Reecriture complete pour telecharger et scanner les vrais tarballs npm. Et les galeres Windows en bonus :

  • execFileSync('npm', ...) echoue sur Windows - npm est un wrapper .cmd
  • tar xzf "C:\path\file.tgz" interprete C: comme un remote host SSH
  • Solution : extracteur tar natif en Node.js (zlib.gunzipSync + parsing des headers tar 512 octets)

Le vrai FPR : 38%

Premier test sur 50 packages reels. Resultat brutal :

PackageScoreCause principale
next10076 dynamic_require, 45 dynamic_import (systeme de plugins)
gatsby10020 dynamic_require, 8 require.cache (HMR)
restify10052 prototype_hook (Request/Response.prototype)
htmx.org100eval pour expressions CSS dynamiques, 5 staged_payload
moleculer100os.hostname + fetch pour metriques Datadog

Next.js a 76 dynamic_require. Un malware en a 1 ou 2.

L'observation cle : volume = legitimite

En analysant les 19 faux positifs initiaux, un pattern est apparu : les packages legitimes produisent des volumes massifs de certains types de menaces, alors que les malwares n'en ont que 1 a 3.

Un framework est bruyant - il utilise des centaines de patterns qui declenchent les heuristiques. Un malware est furtif - il utilise le minimum de techniques pour atteindre son objectif.

Le parcours : 38% → 7%

Pass 1 - Seuils de volume (38% → 19.4%)

  • dynamic_require >10 hits → tous LOW
  • dangerous_call_function >5 hits → tous LOW
  • prototype_hook sur prototypes custom (Request, Response) → MEDIUM
  • require_cache_poison >3 hits → tous LOW

Pass 2 - Filtrage au scanner (19.4% → 17.5%)

  • Expansion de SAFE_ENV_VARS (+13 variables de config)
  • Fichiers dans dist/, build/ → obfuscation LOW
  • Cap scoring pour prototype_hook MEDIUM en volume

Le changement structurel - Per-file max scoring (17.5% → 13%)

Le levier le plus puissant. Au lieu d'additionner les findings de tous les fichiers :

Ancienne formule :
  riskScore = min(100, somme(tous_les_findings))

Nouvelle formule :
  riskScore = min(100, max(scores_par_fichier) + score_package_level)

Un package n'est suspect que si au moins un fichier est suspect. Next.js avec 3162 fichiers .js accumule des findings LOW/MEDIUM partout, mais aucun fichier individuel n'est vraiment suspect.

Passes 3-4 - Precision (13% → 7.4%)

  • Categorisation os.platform/os.arch comme telemetrie (pas credentials)
  • module_compile count-based downgrade (>3 hits → LOW)
  • Skip des aliases npm dans les IOC
  • Fichiers .cjs/.mjs >100KB → bundled output (LOW)

L'analyse par taille

Le FPR n'est pas uniforme. Il depend directement de la taille du package :

CategoriePackagesFPR
Petits (<10 fichiers .js)2906.2%
Moyens (10-50 .js)13511.9%
Gros (50-100 .js)4025.0%
Tres gros (100+ .js)6240.3%

51% du dataset npm sont des petits packages. Pour un dev typique scannant ses dependances, le FPR effectif est plus proche de 6% que de 13%.

Progression complete

VersionFPRCorrection principale
v2.2.738%Premier FPR reel (scan de vrais tarballs)
v2.2.819.4%Seuils de volume par type
v2.2.917.5%Filtrage scanner-level (env vars, dist/)
v2.2.1113.1%Scoring per-file max
v2.3.08.9%Telemetrie vs credentials
v2.3.17.4%Precision heuristique
v2.5.86.0%IOC wildcard audit

Aucune de ces corrections n'a cause de regression sur les adversariaux ou le ground truth. Le principe : on downgrade la severite, on ne supprime jamais les findings.

Lecon

Un scanner qui affiche 0% de faux positifs ment probablement. Le 38% etait embarrassant mais honnete - c'etait la premiere mesure reelle. La suite a montre que la distinction malware/legitime repose souvent sur le volume (un malware est furtif) et la concentration (un malware agit dans 1-2 fichiers, pas dans 3162).

La transparence sur les metriques n'est pas optionnelle. Si on avait continue avec le 0% fictif, toutes les decisions d'amelioration auraient ete basees sur une metrique fausse.