muaddib-scanner 2.8.8 → 2.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/iocs/builtin.yaml +37 -1
- package/package.json +1 -1
- package/src/response/playbooks.js +68 -0
- package/src/rules/index.js +150 -0
- package/src/scanner/ast-detectors.js +190 -1
- package/src/scanner/ast.js +7 -1
- package/src/scanner/obfuscation.js +60 -0
- package/src/scanner/package.js +60 -1
- package/src/scanner/shell.js +3 -1
- package/src/scoring.js +3 -1
package/iocs/builtin.yaml
CHANGED
|
@@ -117,6 +117,32 @@ packages:
|
|
|
117
117
|
source: shai-hulud-v3
|
|
118
118
|
description: "First confirmed v3 payload - testing phase"
|
|
119
119
|
|
|
120
|
+
# GlassWorm hijacked packages (mars 2026)
|
|
121
|
+
- name: "@aifabrix/miso-client"
|
|
122
|
+
version: "4.7.2"
|
|
123
|
+
source: glassworm
|
|
124
|
+
- name: "@iflow-mcp/watercrawl-watercrawl-mcp"
|
|
125
|
+
version: "1.3.0"
|
|
126
|
+
source: glassworm
|
|
127
|
+
- name: "@iflow-mcp/watercrawl-watercrawl-mcp"
|
|
128
|
+
version: "1.3.1"
|
|
129
|
+
source: glassworm
|
|
130
|
+
- name: "@iflow-mcp/watercrawl-watercrawl-mcp"
|
|
131
|
+
version: "1.3.2"
|
|
132
|
+
source: glassworm
|
|
133
|
+
- name: "@iflow-mcp/watercrawl-watercrawl-mcp"
|
|
134
|
+
version: "1.3.3"
|
|
135
|
+
source: glassworm
|
|
136
|
+
- name: "@iflow-mcp/watercrawl-watercrawl-mcp"
|
|
137
|
+
version: "1.3.4"
|
|
138
|
+
source: glassworm
|
|
139
|
+
- name: "react-native-country-select"
|
|
140
|
+
version: "0.3.91"
|
|
141
|
+
source: glassworm
|
|
142
|
+
- name: "react-native-international-phone-number"
|
|
143
|
+
version: "0.11.8"
|
|
144
|
+
source: glassworm
|
|
145
|
+
|
|
120
146
|
# Attaques historiques
|
|
121
147
|
- name: "flatmap-stream"
|
|
122
148
|
version: "0.1.1"
|
|
@@ -177,6 +203,9 @@ files:
|
|
|
177
203
|
- 3nvir0nm3nt.json
|
|
178
204
|
- c9nt3nts.json
|
|
179
205
|
- c0nt3nts.json
|
|
206
|
+
# GlassWorm (mars 2026)
|
|
207
|
+
- i.js
|
|
208
|
+
- init.json
|
|
180
209
|
|
|
181
210
|
hashes:
|
|
182
211
|
# Shai-Hulud v2 payloads
|
|
@@ -185,6 +214,8 @@ hashes:
|
|
|
185
214
|
- "f099c5d9ec417d4445a0328ac0ada9cde79fc37410914103ae9c609cbc0ee068"
|
|
186
215
|
- "a3894003ad1d293ba96d77881ccd2071446dc3f65f434669b49b3da92421901a"
|
|
187
216
|
- "4b2399646573bb737c4969563303d8ee2e9ddbd1b271f1ca9e35ea78062538db"
|
|
217
|
+
# GlassWorm install.js (React Native hijack)
|
|
218
|
+
- "59221aa9623d86c930357dba7e3f54138c7ccbd0daa9c483d766cd8ce1b6ad26"
|
|
188
219
|
|
|
189
220
|
markers:
|
|
190
221
|
# Shai-Hulud v1/v2
|
|
@@ -198,4 +229,9 @@ markers:
|
|
|
198
229
|
# Protestware
|
|
199
230
|
- "peacenotwar"
|
|
200
231
|
# Generic malicious
|
|
201
|
-
- "/dev/tcp"
|
|
232
|
+
- "/dev/tcp"
|
|
233
|
+
# GlassWorm (mars 2026)
|
|
234
|
+
- "lzcdrtfxyqiplpd"
|
|
235
|
+
- "28PKnu7RzizxBzFPoLp69HLXp9bJL3JFtT2s5QzHsEA2"
|
|
236
|
+
- "BjVeAjPrSKFiingBn4vZvghsGj9KCE8AJVtbc9S8o8SC"
|
|
237
|
+
- "6YGcuyFRJKZtcaYCCFba9fScNUvPkGXodXE1mJiSzqDJ"
|
package/package.json
CHANGED
|
@@ -514,6 +514,74 @@ const PLAYBOOKS = {
|
|
|
514
514
|
'Coherence d\'intention detectee — sortie de commande systeme combinee avec exfiltration reseau. ' +
|
|
515
515
|
'Le package execute des commandes et transmet les resultats. Verifier les commandes executees. ' +
|
|
516
516
|
'Supprimer le package si non attendu. Auditer les logs reseau pour identifier les donnees exfiltrees.',
|
|
517
|
+
|
|
518
|
+
unicode_invisible_injection:
|
|
519
|
+
'CRITIQUE: Caracteres Unicode invisibles detectes (zero-width, variation selectors). ' +
|
|
520
|
+
'Technique GlassWorm: du code malveillant est encode via des variation selectors invisibles dans les editeurs. ' +
|
|
521
|
+
'Analyser le fichier avec un editeur hexa. Supprimer le package immediatement. ' +
|
|
522
|
+
'Verifier les autres fichiers du projet pour des injections similaires.',
|
|
523
|
+
|
|
524
|
+
unicode_variation_decoder:
|
|
525
|
+
'CRITIQUE: Decodeur de payload Unicode via variation selectors detecte (.codePointAt + 0xFE00/0xE0100). ' +
|
|
526
|
+
'Signature GlassWorm: le code reconstruit un payload octet par octet a partir de caracteres invisibles. ' +
|
|
527
|
+
'Isoler immediatement. Decoder manuellement les variation selectors pour extraire le payload. Supprimer le package.',
|
|
528
|
+
|
|
529
|
+
blockchain_c2_resolution:
|
|
530
|
+
'Import Solana/Web3 + appel API blockchain C2 (getSignaturesForAddress, getTransaction) detecte. ' +
|
|
531
|
+
'Technique GlassWorm: la blockchain sert de dead drop resolver pour obtenir l\'adresse C2 via le champ memo. ' +
|
|
532
|
+
'Cout de rotation: 0.000005 SOL par changement d\'adresse C2 — censorship-resistant. ' +
|
|
533
|
+
'Bloquer les connexions vers les RPC Solana. Supprimer le package.',
|
|
534
|
+
|
|
535
|
+
blockchain_rpc_endpoint:
|
|
536
|
+
'Endpoint RPC blockchain hardcode detecte (Solana mainnet, Infura Ethereum). ' +
|
|
537
|
+
'Dans un package non-crypto, cela indique un potentiel canal C2 via blockchain. ' +
|
|
538
|
+
'Verifier le contexte: si le package n\'a rien a voir avec la blockchain, supprimer immediatement.',
|
|
539
|
+
|
|
540
|
+
bin_field_hijack:
|
|
541
|
+
'CRITIQUE: Le champ "bin" de package.json shadow une commande systeme (node, npm, git, bash, etc.). ' +
|
|
542
|
+
'A l\'installation, npm cree un symlink dans node_modules/.bin/ qui intercepte la commande reelle. ' +
|
|
543
|
+
'Tous les npm scripts executeront le code malveillant au lieu de la vraie commande. ' +
|
|
544
|
+
'NE PAS installer. Si deja installe: rm -rf node_modules && npm cache clean --force && npm install.',
|
|
545
|
+
|
|
546
|
+
git_dependency_rce:
|
|
547
|
+
'Dependance utilisant une URL git+ ou git://. Vecteur d\'attaque PackageGate: si le package contient ' +
|
|
548
|
+
'un .npmrc avec git=./malicious.sh, npm executera le script au lieu de git, meme avec --ignore-scripts. ' +
|
|
549
|
+
'Verifier le contenu du .npmrc. Ne pas installer de packages avec des dependances git non vérifiées.',
|
|
550
|
+
|
|
551
|
+
npmrc_git_override:
|
|
552
|
+
'CRITIQUE: Fichier .npmrc contient git= override — technique PackageGate. Le binaire git est remplace ' +
|
|
553
|
+
'par un script controle par l\'attaquant. TOUTE operation git (clone, fetch, pull) executera le script malveillant. ' +
|
|
554
|
+
'NE PAS installer. Si deja installe: supprimer le package, verifier .npmrc, reinstaller git.',
|
|
555
|
+
|
|
556
|
+
node_modules_write:
|
|
557
|
+
'CRITIQUE: Le code ecrit dans node_modules/ — technique de propagation worm Shai-Hulud 2.0. ' +
|
|
558
|
+
'Le malware modifie d\'autres packages installes (ethers, webpack, etc.) pour injecter un backdoor persistent. ' +
|
|
559
|
+
'Verifier l\'integrite de tous les packages: rm -rf node_modules && npm install. ' +
|
|
560
|
+
'Auditer les fichiers modifies. Regenerer tous les secrets si le code a ete execute.',
|
|
561
|
+
|
|
562
|
+
bun_runtime_evasion:
|
|
563
|
+
'Invocation du runtime Bun detectee — technique Shai-Hulud 2.0. Le payload est execute via bun run ' +
|
|
564
|
+
'au lieu de node, echappant a toutes les sandboxes Node.js et au monitoring (--experimental-permission). ' +
|
|
565
|
+
'Verifier si bun est installe: which bun. Supprimer le package. ' +
|
|
566
|
+
'Auditer les processus: ps aux | grep bun.',
|
|
567
|
+
|
|
568
|
+
static_timer_bomb:
|
|
569
|
+
'Timer avec delai > 1h detecte dans l\'analyse statique (setTimeout/setInterval). ' +
|
|
570
|
+
'Technique PhantomRaven: le payload s\'active 48h+ apres l\'installation pour echapper aux sandboxes. ' +
|
|
571
|
+
'Analyser le callback du timer pour identifier le payload retarde. ' +
|
|
572
|
+
'Si delai > 24h: fort indicateur de time-bomb malware. NE PAS installer.',
|
|
573
|
+
|
|
574
|
+
npm_publish_worm:
|
|
575
|
+
'CRITIQUE: exec("npm publish") detecte — propagation worm. Le code utilise des tokens npm voles ' +
|
|
576
|
+
'pour publier des versions infectees des packages de la victime. Technique Shai-Hulud 1.0 et 2.0. ' +
|
|
577
|
+
'Isoler immediatement la machine. Revoquer les tokens npm: npm token revoke. ' +
|
|
578
|
+
'Verifier les packages publies: npm profile ls. Signaler sur npm.',
|
|
579
|
+
|
|
580
|
+
ollama_local_llm:
|
|
581
|
+
'Reference au port Ollama (11434) detectee. PhantomRaven Wave 4 utilise un LLM local (DeepSeek Coder) ' +
|
|
582
|
+
'pour reecrire le malware a chaque execution, evitant la detection par signature. Moteur polymorphe. ' +
|
|
583
|
+
'Verifier si Ollama est installe: curl http://localhost:11434/api/tags. ' +
|
|
584
|
+
'Aucun package npm legitime n\'appelle un LLM local. Supprimer le package.',
|
|
517
585
|
};
|
|
518
586
|
|
|
519
587
|
function getPlaybook(threatType) {
|
package/src/rules/index.js
CHANGED
|
@@ -1378,6 +1378,106 @@ const RULES = {
|
|
|
1378
1378
|
],
|
|
1379
1379
|
mitre: 'T1557'
|
|
1380
1380
|
},
|
|
1381
|
+
// Package manifest detections (v2.8.9)
|
|
1382
|
+
bin_field_hijack: {
|
|
1383
|
+
id: 'MUADDIB-PKG-013',
|
|
1384
|
+
name: 'Bin Field PATH Hijack',
|
|
1385
|
+
severity: 'CRITICAL',
|
|
1386
|
+
confidence: 'high',
|
|
1387
|
+
description: 'Le champ "bin" de package.json shadow une commande systeme (node, npm, git, bash, etc.). A l\'install, npm cree un symlink dans node_modules/.bin/ qui intercepte la commande reelle pour tous les npm scripts.',
|
|
1388
|
+
references: [
|
|
1389
|
+
'https://socket.dev/blog/2025-supply-chain-report',
|
|
1390
|
+
'https://www.wiz.io/blog/shai-hulud-npm-supply-chain-attack'
|
|
1391
|
+
],
|
|
1392
|
+
mitre: 'T1574.007'
|
|
1393
|
+
},
|
|
1394
|
+
git_dependency_rce: {
|
|
1395
|
+
id: 'MUADDIB-PKG-014',
|
|
1396
|
+
name: 'Git Dependency RCE (PackageGate)',
|
|
1397
|
+
severity: 'HIGH',
|
|
1398
|
+
confidence: 'medium',
|
|
1399
|
+
description: 'Dependance utilisant une URL git+ ou git://. Vecteur PackageGate: un .npmrc malveillant peut overrider le binaire git, permettant l\'execution de code meme avec --ignore-scripts.',
|
|
1400
|
+
references: [
|
|
1401
|
+
'https://socket.dev/blog/packagegate-npm-rce',
|
|
1402
|
+
'https://attack.mitre.org/techniques/T1195/002/'
|
|
1403
|
+
],
|
|
1404
|
+
mitre: 'T1195.002'
|
|
1405
|
+
},
|
|
1406
|
+
npmrc_git_override: {
|
|
1407
|
+
id: 'MUADDIB-PKG-015',
|
|
1408
|
+
name: '.npmrc Git Binary Override',
|
|
1409
|
+
severity: 'CRITICAL',
|
|
1410
|
+
confidence: 'high',
|
|
1411
|
+
description: 'Fichier .npmrc contient git= override — technique PackageGate: remplace le binaire git par un script controle par l\'attaquant.',
|
|
1412
|
+
references: [
|
|
1413
|
+
'https://socket.dev/blog/packagegate-npm-rce'
|
|
1414
|
+
],
|
|
1415
|
+
mitre: 'T1195.002'
|
|
1416
|
+
},
|
|
1417
|
+
|
|
1418
|
+
// AST detections (v2.8.9 — supply-chain gaps)
|
|
1419
|
+
node_modules_write: {
|
|
1420
|
+
id: 'MUADDIB-AST-048',
|
|
1421
|
+
name: 'Write to node_modules/ (Worm Propagation)',
|
|
1422
|
+
severity: 'CRITICAL',
|
|
1423
|
+
confidence: 'high',
|
|
1424
|
+
description: 'writeFileSync/writeFile/appendFileSync ciblant node_modules/ — technique de propagation worm Shai-Hulud 2.0: modifie d\'autres packages installes pour injecter un backdoor persistent.',
|
|
1425
|
+
references: [
|
|
1426
|
+
'https://www.wiz.io/blog/shai-hulud-npm-supply-chain-attack',
|
|
1427
|
+
'https://attack.mitre.org/techniques/T1195/002/'
|
|
1428
|
+
],
|
|
1429
|
+
mitre: 'T1195.002'
|
|
1430
|
+
},
|
|
1431
|
+
bun_runtime_evasion: {
|
|
1432
|
+
id: 'MUADDIB-AST-049',
|
|
1433
|
+
name: 'Bun Runtime Evasion',
|
|
1434
|
+
severity: 'HIGH',
|
|
1435
|
+
confidence: 'medium',
|
|
1436
|
+
description: 'Invocation du runtime Bun (bun run/exec/install) via exec/spawn — technique Shai-Hulud 2.0: utilise un runtime alternatif pour echapper aux sandboxes et monitoring Node.js.',
|
|
1437
|
+
references: [
|
|
1438
|
+
'https://www.wiz.io/blog/shai-hulud-npm-supply-chain-attack',
|
|
1439
|
+
'https://attack.mitre.org/techniques/T1059/'
|
|
1440
|
+
],
|
|
1441
|
+
mitre: 'T1059'
|
|
1442
|
+
},
|
|
1443
|
+
static_timer_bomb: {
|
|
1444
|
+
id: 'MUADDIB-AST-050',
|
|
1445
|
+
name: 'Static Timer Bomb',
|
|
1446
|
+
severity: 'MEDIUM',
|
|
1447
|
+
confidence: 'medium',
|
|
1448
|
+
description: 'setTimeout/setInterval avec delai > 1h detecte statiquement. PhantomRaven active le 2nd stage 48h+ apres install. Evasion temporelle: le payload s\'active bien apres l\'installation pour echapper aux sandboxes.',
|
|
1449
|
+
references: [
|
|
1450
|
+
'https://www.sonatype.com/blog/phantomraven-supply-chain-attack',
|
|
1451
|
+
'https://attack.mitre.org/techniques/T1497/003/'
|
|
1452
|
+
],
|
|
1453
|
+
mitre: 'T1497.003'
|
|
1454
|
+
},
|
|
1455
|
+
npm_publish_worm: {
|
|
1456
|
+
id: 'MUADDIB-AST-051',
|
|
1457
|
+
name: 'npm publish Worm Propagation',
|
|
1458
|
+
severity: 'CRITICAL',
|
|
1459
|
+
confidence: 'high',
|
|
1460
|
+
description: 'exec("npm publish") detecte — technique de propagation worm Shai-Hulud: utilise les tokens npm voles pour publier des versions infectees des packages de la victime.',
|
|
1461
|
+
references: [
|
|
1462
|
+
'https://blog.phylum.io/shai-hulud-npm-worm',
|
|
1463
|
+
'https://www.wiz.io/blog/shai-hulud-npm-supply-chain-attack',
|
|
1464
|
+
'https://attack.mitre.org/techniques/T1195/002/'
|
|
1465
|
+
],
|
|
1466
|
+
mitre: 'T1195.002'
|
|
1467
|
+
},
|
|
1468
|
+
ollama_local_llm: {
|
|
1469
|
+
id: 'MUADDIB-AST-052',
|
|
1470
|
+
name: 'Ollama Local LLM (Polymorphic Engine)',
|
|
1471
|
+
severity: 'HIGH',
|
|
1472
|
+
confidence: 'medium',
|
|
1473
|
+
description: 'Reference au port 11434 (Ollama) detectee. PhantomRaven Wave 4 utilise un LLM local pour reecrire le malware et eviter la detection signature. Moteur polymorphe.',
|
|
1474
|
+
references: [
|
|
1475
|
+
'https://www.sonatype.com/blog/phantomraven-supply-chain-attack',
|
|
1476
|
+
'https://attack.mitre.org/techniques/T1027/005/'
|
|
1477
|
+
],
|
|
1478
|
+
mitre: 'T1027.005'
|
|
1479
|
+
},
|
|
1480
|
+
|
|
1381
1481
|
// Shell IFS evasion rules (v2.6.9)
|
|
1382
1482
|
curl_ifs_evasion: {
|
|
1383
1483
|
id: 'MUADDIB-SHELL-016',
|
|
@@ -1444,6 +1544,56 @@ const RULES = {
|
|
|
1444
1544
|
],
|
|
1445
1545
|
mitre: 'T1059'
|
|
1446
1546
|
},
|
|
1547
|
+
|
|
1548
|
+
// GlassWorm detections (mars 2026)
|
|
1549
|
+
unicode_invisible_injection: {
|
|
1550
|
+
id: 'MUADDIB-OBF-003',
|
|
1551
|
+
name: 'Unicode Invisible Character Injection',
|
|
1552
|
+
severity: 'CRITICAL',
|
|
1553
|
+
confidence: 'high',
|
|
1554
|
+
description: 'Caracteres Unicode invisibles detectes (zero-width, variation selectors). Technique GlassWorm: encodage de payload malveillant via variation selectors (U+FE00-FE0F, U+E0100-E01EF) invisible dans les editeurs.',
|
|
1555
|
+
references: [
|
|
1556
|
+
'https://www.aikido.dev/blog/glassworm-returns-unicode-attack-github-npm-vscode',
|
|
1557
|
+
'https://attack.mitre.org/techniques/T1027/'
|
|
1558
|
+
],
|
|
1559
|
+
mitre: 'T1027'
|
|
1560
|
+
},
|
|
1561
|
+
unicode_variation_decoder: {
|
|
1562
|
+
id: 'MUADDIB-AST-053',
|
|
1563
|
+
name: 'Unicode Variation Selector Decoder',
|
|
1564
|
+
severity: 'CRITICAL',
|
|
1565
|
+
confidence: 'high',
|
|
1566
|
+
description: 'Decodeur de payload Unicode via variation selectors (.codePointAt + 0xFE00/0xE0100). Signature GlassWorm: le code reconstruit un payload octet par octet a partir de caracteres invisibles.',
|
|
1567
|
+
references: [
|
|
1568
|
+
'https://www.koi.security/blog/glassworm-first-self-propagating-worm-using-invisible-code-hits-openvsx-marketplace',
|
|
1569
|
+
'https://attack.mitre.org/techniques/T1140/'
|
|
1570
|
+
],
|
|
1571
|
+
mitre: 'T1140'
|
|
1572
|
+
},
|
|
1573
|
+
blockchain_c2_resolution: {
|
|
1574
|
+
id: 'MUADDIB-AST-054',
|
|
1575
|
+
name: 'Blockchain C2 Resolution (Dead Drop)',
|
|
1576
|
+
severity: 'HIGH',
|
|
1577
|
+
confidence: 'high',
|
|
1578
|
+
description: 'Import Solana/Web3 + appel API C2 (getSignaturesForAddress, getTransaction). Technique GlassWorm: la blockchain sert de dead drop resolver pour obtenir l\'adresse C2 via le champ memo des transactions.',
|
|
1579
|
+
references: [
|
|
1580
|
+
'https://www.sonatype.com/blog/hijacked-npm-packages-deliver-malware-via-solana-linked-to-glassworm',
|
|
1581
|
+
'https://attack.mitre.org/techniques/T1102/'
|
|
1582
|
+
],
|
|
1583
|
+
mitre: 'T1102'
|
|
1584
|
+
},
|
|
1585
|
+
blockchain_rpc_endpoint: {
|
|
1586
|
+
id: 'MUADDIB-AST-055',
|
|
1587
|
+
name: 'Hardcoded Blockchain RPC Endpoint',
|
|
1588
|
+
severity: 'MEDIUM',
|
|
1589
|
+
confidence: 'medium',
|
|
1590
|
+
description: 'Endpoint RPC blockchain hardcode (Solana mainnet, Infura Ethereum). Dans un package non-crypto, indique un potentiel canal C2 via blockchain.',
|
|
1591
|
+
references: [
|
|
1592
|
+
'https://www.koi.security/blog/glassworm-first-self-propagating-worm-using-invisible-code-hits-openvsx-marketplace',
|
|
1593
|
+
'https://attack.mitre.org/techniques/T1102/'
|
|
1594
|
+
],
|
|
1595
|
+
mitre: 'T1102'
|
|
1596
|
+
},
|
|
1447
1597
|
};
|
|
1448
1598
|
|
|
1449
1599
|
function getRule(type) {
|
|
@@ -170,7 +170,11 @@ const GIT_HOOKS = [
|
|
|
170
170
|
const SUSPICIOUS_DOMAINS_HIGH = [
|
|
171
171
|
'oastify.com', 'oast.fun', 'oast.me', 'oast.live',
|
|
172
172
|
'burpcollaborator.net', 'webhook.site', 'pipedream.net',
|
|
173
|
-
'requestbin.com', 'hookbin.com', 'canarytokens.com'
|
|
173
|
+
'requestbin.com', 'hookbin.com', 'canarytokens.com',
|
|
174
|
+
// GlassWorm C2 IPs (mars 2026)
|
|
175
|
+
'217.69.3.218', '217.69.3.152',
|
|
176
|
+
'199.247.10.166', '199.247.13.106',
|
|
177
|
+
'140.82.52.31', '45.32.150.251'
|
|
174
178
|
];
|
|
175
179
|
|
|
176
180
|
// Suspicious tunnel/proxy domains (MEDIUM severity)
|
|
@@ -198,6 +202,27 @@ const SANDBOX_INDICATORS = [
|
|
|
198
202
|
'/proc/self/cgroup'
|
|
199
203
|
];
|
|
200
204
|
|
|
205
|
+
// Blockchain RPC endpoints — potential C2 channel via blockchain (GlassWorm)
|
|
206
|
+
const BLOCKCHAIN_RPC_ENDPOINTS = [
|
|
207
|
+
'api.mainnet-beta.solana.com',
|
|
208
|
+
'api.devnet.solana.com',
|
|
209
|
+
'api.testnet.solana.com',
|
|
210
|
+
'mainnet.infura.io',
|
|
211
|
+
'rpc.ankr.com'
|
|
212
|
+
];
|
|
213
|
+
|
|
214
|
+
// Solana/Web3 C2 methods — used for dead drop resolver (GlassWorm)
|
|
215
|
+
const SOLANA_C2_METHODS = [
|
|
216
|
+
'getSignaturesForAddress', 'getAccountInfo', 'getTransaction',
|
|
217
|
+
'getConfirmedSignaturesForAddress2', 'getParsedTransaction'
|
|
218
|
+
];
|
|
219
|
+
|
|
220
|
+
// Solana/Web3 package names
|
|
221
|
+
const SOLANA_PACKAGES = ['@solana/web3.js', 'solana-web3.js', '@solana/web3'];
|
|
222
|
+
|
|
223
|
+
// Variation selector constants (GlassWorm decoder signature)
|
|
224
|
+
const VARIATION_SELECTOR_CONSTS = [0xFE00, 0xFE0F, 0xE0100, 0xE01EF];
|
|
225
|
+
|
|
201
226
|
// ============================================
|
|
202
227
|
// HELPER FUNCTIONS
|
|
203
228
|
// ============================================
|
|
@@ -647,6 +672,10 @@ function handleCallExpression(node, ctx) {
|
|
|
647
672
|
if (reqStr && /\.node\s*$/.test(reqStr)) {
|
|
648
673
|
ctx.hasRequireNodeFile = true;
|
|
649
674
|
}
|
|
675
|
+
// GlassWorm: track Solana/Web3 require imports for compound blockchain C2 detection
|
|
676
|
+
if (reqStr && SOLANA_PACKAGES.some(pkg => reqStr === pkg)) {
|
|
677
|
+
ctx.hasSolanaImport = true;
|
|
678
|
+
}
|
|
650
679
|
}
|
|
651
680
|
|
|
652
681
|
// Detect exec/execSync with dangerous shell commands (direct or via MemberExpression)
|
|
@@ -705,6 +734,26 @@ function handleCallExpression(node, ctx) {
|
|
|
705
734
|
break;
|
|
706
735
|
}
|
|
707
736
|
}
|
|
737
|
+
|
|
738
|
+
// Bun runtime evasion: exec/spawn using bun instead of node (Shai-Hulud 2.0)
|
|
739
|
+
if (/\bbun\s+(run|exec|install|x)\b/.test(cmdStr)) {
|
|
740
|
+
ctx.threats.push({
|
|
741
|
+
type: 'bun_runtime_evasion',
|
|
742
|
+
severity: 'HIGH',
|
|
743
|
+
message: `Bun runtime invocation detected: "${cmdStr.slice(0, 80)}" — alternative runtime to evade Node.js monitoring/sandboxing.`,
|
|
744
|
+
file: ctx.relFile
|
|
745
|
+
});
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
// Worm propagation: npm publish / npm adduser / npm token create (Shai-Hulud)
|
|
749
|
+
if (/\bnpm\s+(publish|adduser|token\s+create|login)\b/.test(cmdStr)) {
|
|
750
|
+
ctx.threats.push({
|
|
751
|
+
type: 'npm_publish_worm',
|
|
752
|
+
severity: 'CRITICAL',
|
|
753
|
+
message: `exec("${cmdStr.slice(0, 80)}") — worm propagation: publishing packages with stolen credentials.`,
|
|
754
|
+
file: ctx.relFile
|
|
755
|
+
});
|
|
756
|
+
}
|
|
708
757
|
}
|
|
709
758
|
}
|
|
710
759
|
|
|
@@ -785,6 +834,30 @@ function handleCallExpression(node, ctx) {
|
|
|
785
834
|
}
|
|
786
835
|
}
|
|
787
836
|
|
|
837
|
+
// Detect spawn('bun', ['run', ...]) — Bun runtime evasion via spawn
|
|
838
|
+
if ((callName === 'spawn' || callName === 'execFile') && node.arguments.length >= 1) {
|
|
839
|
+
const bunArg = node.arguments[0];
|
|
840
|
+
if (bunArg.type === 'Literal' && typeof bunArg.value === 'string' && bunArg.value === 'bun') {
|
|
841
|
+
// Check the args array for run/exec/install/x
|
|
842
|
+
const argsArr = node.arguments[1];
|
|
843
|
+
let bunCmd = 'bun';
|
|
844
|
+
if (argsArr?.type === 'ArrayExpression' && argsArr.elements.length > 0) {
|
|
845
|
+
const firstEl = argsArr.elements[0];
|
|
846
|
+
if (firstEl?.type === 'Literal' && typeof firstEl.value === 'string') {
|
|
847
|
+
bunCmd = `bun ${firstEl.value}`;
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
if (/\bbun\s*(run|exec|install|x)?$/.test(bunCmd) || bunCmd === 'bun') {
|
|
851
|
+
ctx.threats.push({
|
|
852
|
+
type: 'bun_runtime_evasion',
|
|
853
|
+
severity: 'HIGH',
|
|
854
|
+
message: `spawn("bun", [...]) — Bun runtime invocation to evade Node.js monitoring/sandboxing.`,
|
|
855
|
+
file: ctx.relFile
|
|
856
|
+
});
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
|
|
788
861
|
// Detect spawn/fork with {detached: true}
|
|
789
862
|
if ((callName === 'spawn' || callName === 'fork') && node.arguments.length >= 2) {
|
|
790
863
|
const lastArg = node.arguments[node.arguments.length - 1];
|
|
@@ -836,6 +909,27 @@ function handleCallExpression(node, ctx) {
|
|
|
836
909
|
}
|
|
837
910
|
}
|
|
838
911
|
|
|
912
|
+
// Detect writes to node_modules/ — worm propagation / package patching (Shai-Hulud 2.0)
|
|
913
|
+
if (node.callee.type === 'MemberExpression' && node.callee.property?.type === 'Identifier') {
|
|
914
|
+
const nmWriteMethod = node.callee.property.name;
|
|
915
|
+
if (['writeFileSync', 'writeFile', 'appendFileSync'].includes(nmWriteMethod) && node.arguments.length >= 2) {
|
|
916
|
+
const nmPathArg = node.arguments[0];
|
|
917
|
+
let nmPathStr = extractStringValueDeep(nmPathArg);
|
|
918
|
+
// Also resolve variable indirection
|
|
919
|
+
if (!nmPathStr && nmPathArg?.type === 'Identifier' && ctx.stringVarValues?.has(nmPathArg.name)) {
|
|
920
|
+
nmPathStr = ctx.stringVarValues.get(nmPathArg.name);
|
|
921
|
+
}
|
|
922
|
+
if (nmPathStr && /node_modules[/\\]/.test(nmPathStr)) {
|
|
923
|
+
ctx.threats.push({
|
|
924
|
+
type: 'node_modules_write',
|
|
925
|
+
severity: 'CRITICAL',
|
|
926
|
+
message: `${nmWriteMethod}() targeting node_modules/ path: "${nmPathStr.substring(0, 80)}" — package patching / worm propagation technique.`,
|
|
927
|
+
file: ctx.relFile
|
|
928
|
+
});
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
|
|
839
933
|
// Detect fs.mkdirSync creating .github/workflows
|
|
840
934
|
if (node.callee.type === 'MemberExpression' && node.callee.property?.type === 'Identifier') {
|
|
841
935
|
const mkdirMethod = node.callee.property.name;
|
|
@@ -1200,6 +1294,24 @@ function handleCallExpression(node, ctx) {
|
|
|
1200
1294
|
file: ctx.relFile
|
|
1201
1295
|
});
|
|
1202
1296
|
}
|
|
1297
|
+
|
|
1298
|
+
// Static timer bomb: setTimeout/setInterval with delay > 1 hour (PhantomRaven 48h delay)
|
|
1299
|
+
if (node.arguments.length >= 2) {
|
|
1300
|
+
const delayArg = node.arguments[1];
|
|
1301
|
+
let delayMs = null;
|
|
1302
|
+
if (delayArg.type === 'Literal' && typeof delayArg.value === 'number') {
|
|
1303
|
+
delayMs = delayArg.value;
|
|
1304
|
+
}
|
|
1305
|
+
if (delayMs !== null && delayMs > 3600000) { // > 1 hour
|
|
1306
|
+
const hours = (delayMs / 3600000).toFixed(1);
|
|
1307
|
+
ctx.threats.push({
|
|
1308
|
+
type: 'static_timer_bomb',
|
|
1309
|
+
severity: delayMs > 86400000 ? 'HIGH' : 'MEDIUM', // > 24h = HIGH
|
|
1310
|
+
message: `${callName}() with ${hours}h delay (${delayMs}ms) — time-bomb evasion: payload activates long after install.`,
|
|
1311
|
+
file: ctx.relFile
|
|
1312
|
+
});
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1203
1315
|
}
|
|
1204
1316
|
|
|
1205
1317
|
// Detect eval.call(null, code) / eval.apply(null, [code]) / Function.call/apply
|
|
@@ -1570,6 +1682,10 @@ function handleImportExpression(node, ctx) {
|
|
|
1570
1682
|
file: ctx.relFile
|
|
1571
1683
|
});
|
|
1572
1684
|
}
|
|
1685
|
+
// GlassWorm: track Solana/Web3 dynamic import for compound blockchain C2 detection
|
|
1686
|
+
if (SOLANA_PACKAGES.some(pkg => src.value === pkg)) {
|
|
1687
|
+
ctx.hasSolanaImport = true;
|
|
1688
|
+
}
|
|
1573
1689
|
} else {
|
|
1574
1690
|
ctx.threats.push({
|
|
1575
1691
|
type: 'dynamic_import',
|
|
@@ -1727,6 +1843,45 @@ function handleLiteral(node, ctx) {
|
|
|
1727
1843
|
break;
|
|
1728
1844
|
}
|
|
1729
1845
|
}
|
|
1846
|
+
|
|
1847
|
+
// Ollama LLM local: polymorphic engine indicator (PhantomRaven Wave 4)
|
|
1848
|
+
// Port 11434 is Ollama's default port. Legitimate packages don't call local LLMs.
|
|
1849
|
+
if (/(?:localhost|127\.0\.0\.1):11434/.test(node.value)) {
|
|
1850
|
+
ctx.threats.push({
|
|
1851
|
+
type: 'ollama_local_llm',
|
|
1852
|
+
severity: 'HIGH',
|
|
1853
|
+
message: `Reference to Ollama LLM API (${node.value.slice(0, 60)}) — polymorphic malware engine: uses local LLM to rewrite code and evade detection.`,
|
|
1854
|
+
file: ctx.relFile
|
|
1855
|
+
});
|
|
1856
|
+
}
|
|
1857
|
+
|
|
1858
|
+
// Blockchain RPC endpoints — potential C2 channel (GlassWorm)
|
|
1859
|
+
for (const endpoint of BLOCKCHAIN_RPC_ENDPOINTS) {
|
|
1860
|
+
if (lowerVal.includes(endpoint)) {
|
|
1861
|
+
ctx.threats.push({
|
|
1862
|
+
type: 'blockchain_rpc_endpoint',
|
|
1863
|
+
severity: 'MEDIUM',
|
|
1864
|
+
message: `Hardcoded blockchain RPC endpoint "${endpoint}" — potential blockchain C2 channel.`,
|
|
1865
|
+
file: ctx.relFile
|
|
1866
|
+
});
|
|
1867
|
+
break;
|
|
1868
|
+
}
|
|
1869
|
+
}
|
|
1870
|
+
|
|
1871
|
+
// Track Solana C2 method names in string literals (for compound detection)
|
|
1872
|
+
for (const method of SOLANA_C2_METHODS) {
|
|
1873
|
+
if (node.value === method || node.value.includes(method)) {
|
|
1874
|
+
ctx.hasSolanaC2Method = true;
|
|
1875
|
+
break;
|
|
1876
|
+
}
|
|
1877
|
+
}
|
|
1878
|
+
}
|
|
1879
|
+
|
|
1880
|
+
// Track variation selector constants in numeric literals (GlassWorm decoder)
|
|
1881
|
+
if (typeof node.value === 'number') {
|
|
1882
|
+
if (VARIATION_SELECTOR_CONSTS.includes(node.value)) {
|
|
1883
|
+
ctx.hasVariationSelectorConst = true;
|
|
1884
|
+
}
|
|
1730
1885
|
}
|
|
1731
1886
|
}
|
|
1732
1887
|
|
|
@@ -1901,6 +2056,16 @@ function handleMemberExpression(node, ctx) {
|
|
|
1901
2056
|
});
|
|
1902
2057
|
}
|
|
1903
2058
|
|
|
2059
|
+
// GlassWorm: track .codePointAt() calls (variation selector decoder pattern)
|
|
2060
|
+
if (node.property?.type === 'Identifier' && node.property.name === 'codePointAt') {
|
|
2061
|
+
ctx.hasCodePointAt = true;
|
|
2062
|
+
}
|
|
2063
|
+
|
|
2064
|
+
// GlassWorm: track Solana C2 method calls (e.g., connection.getSignaturesForAddress)
|
|
2065
|
+
if (node.property?.type === 'Identifier' && SOLANA_C2_METHODS.includes(node.property.name)) {
|
|
2066
|
+
ctx.hasSolanaC2Method = true;
|
|
2067
|
+
}
|
|
2068
|
+
|
|
1904
2069
|
if (
|
|
1905
2070
|
node.object?.object?.name === 'process' &&
|
|
1906
2071
|
node.object?.property?.name === 'env'
|
|
@@ -2205,6 +2370,30 @@ function handlePostWalk(ctx) {
|
|
|
2205
2370
|
file: ctx.relFile
|
|
2206
2371
|
});
|
|
2207
2372
|
}
|
|
2373
|
+
|
|
2374
|
+
// GlassWorm: Unicode variation selector decoder = .codePointAt + variation selector constants
|
|
2375
|
+
if (ctx.hasCodePointAt && ctx.hasVariationSelectorConst) {
|
|
2376
|
+
ctx.threats.push({
|
|
2377
|
+
type: 'unicode_variation_decoder',
|
|
2378
|
+
severity: 'CRITICAL',
|
|
2379
|
+
message: 'Unicode variation selector decoder: .codePointAt() + 0xFE00/0xE0100 constants — GlassWorm payload reconstruction from invisible characters.',
|
|
2380
|
+
file: ctx.relFile
|
|
2381
|
+
});
|
|
2382
|
+
}
|
|
2383
|
+
|
|
2384
|
+
// GlassWorm: Blockchain C2 resolution = Solana import + C2 method
|
|
2385
|
+
// CRITICAL if combined with eval/exec, HIGH otherwise
|
|
2386
|
+
if (ctx.hasSolanaImport && ctx.hasSolanaC2Method) {
|
|
2387
|
+
ctx.threats.push({
|
|
2388
|
+
type: 'blockchain_c2_resolution',
|
|
2389
|
+
severity: ctx.hasDynamicExec ? 'CRITICAL' : 'HIGH',
|
|
2390
|
+
message: 'Solana/Web3 import + blockchain C2 method (getSignaturesForAddress/getTransaction) — ' +
|
|
2391
|
+
(ctx.hasDynamicExec
|
|
2392
|
+
? 'dead drop resolver with dynamic execution. GlassWorm blockchain C2 pattern confirmed.'
|
|
2393
|
+
: 'potential dead drop resolver. GlassWorm technique: C2 address rotated via Solana memo field.'),
|
|
2394
|
+
file: ctx.relFile
|
|
2395
|
+
});
|
|
2396
|
+
}
|
|
2208
2397
|
}
|
|
2209
2398
|
|
|
2210
2399
|
function handleWithStatement(node, ctx) {
|
package/src/scanner/ast.js
CHANGED
|
@@ -160,7 +160,13 @@ function analyzeFile(content, filePath, basePath) {
|
|
|
160
160
|
// C10: Hash verification — legitimate binary installers verify checksums
|
|
161
161
|
// Requires BOTH createHash() call AND .digest() call — false positives from
|
|
162
162
|
// standalone mentions of 'sha256' or 'integrity' in comments/descriptions
|
|
163
|
-
hasHashVerification: /\bcreateHash\s*\(/.test(content) && /\.digest\s*\(/.test(content)
|
|
163
|
+
hasHashVerification: /\bcreateHash\s*\(/.test(content) && /\.digest\s*\(/.test(content),
|
|
164
|
+
// GlassWorm: variation selector decoder pattern (.codePointAt + 0xFE00/0xE0100)
|
|
165
|
+
hasCodePointAt: false,
|
|
166
|
+
hasVariationSelectorConst: false,
|
|
167
|
+
// GlassWorm: blockchain C2 resolution (Solana import + C2 method + dynamic exec)
|
|
168
|
+
hasSolanaImport: false,
|
|
169
|
+
hasSolanaC2Method: false
|
|
164
170
|
};
|
|
165
171
|
|
|
166
172
|
// Compute fetchOnlySafeDomains: check if ALL URLs in file point to known registries
|
|
@@ -70,6 +70,18 @@ function detectObfuscation(targetPath) {
|
|
|
70
70
|
signals.push('base64_eval');
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
+
// 7. Unicode invisible character injection (GlassWorm — mars 2026)
|
|
74
|
+
// Detects zero-width chars, variation selectors, tag characters embedded in source
|
|
75
|
+
const invisibleCount = countInvisibleUnicode(content);
|
|
76
|
+
if (invisibleCount >= 3) {
|
|
77
|
+
threats.push({
|
|
78
|
+
type: 'unicode_invisible_injection',
|
|
79
|
+
severity: isPackageOutput ? 'LOW' : 'CRITICAL',
|
|
80
|
+
message: `${invisibleCount} invisible Unicode characters detected (zero-width, variation selectors, tag chars). GlassWorm technique: payload encoded via invisible codepoints.`,
|
|
81
|
+
file: relativePath
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
73
85
|
// Hex/unicode escapes alone are not obfuscation (e.g. lodash Unicode char tables).
|
|
74
86
|
// Only count them when combined with strong obfuscation signals.
|
|
75
87
|
const hasStrongSignals = signals.some(s => s !== 'hex_escapes' && s !== 'unicode_escapes');
|
|
@@ -130,4 +142,52 @@ function hasLargeStringArray(content) {
|
|
|
130
142
|
return false;
|
|
131
143
|
}
|
|
132
144
|
|
|
145
|
+
/**
|
|
146
|
+
* Count invisible Unicode codepoints in content (GlassWorm detection).
|
|
147
|
+
* Covers BMP zero-width chars, variation selectors, and supplementary plane
|
|
148
|
+
* tag characters / variation selectors supplement via codePointAt iteration.
|
|
149
|
+
*
|
|
150
|
+
* Codepoints detected:
|
|
151
|
+
* - U+200B, U+200C, U+200D (zero-width space/joiner/non-joiner)
|
|
152
|
+
* - U+FEFF (BOM — only if position > 0; pos 0 is legitimate BOM)
|
|
153
|
+
* - U+2060 (word joiner), U+180E (Mongolian vowel separator)
|
|
154
|
+
* - U+FE00-U+FE0F (variation selectors — GlassWorm 256-value encoding)
|
|
155
|
+
* - U+E0100-U+E01EF (variation selectors supplement)
|
|
156
|
+
* - U+E0001-U+E007F (tag characters)
|
|
157
|
+
*/
|
|
158
|
+
function countInvisibleUnicode(content) {
|
|
159
|
+
let count = 0;
|
|
160
|
+
for (let i = 0; i < content.length; i++) {
|
|
161
|
+
const cp = content.codePointAt(i);
|
|
162
|
+
// BMP invisible chars
|
|
163
|
+
if (cp === 0x200B || cp === 0x200C || cp === 0x200D ||
|
|
164
|
+
cp === 0x2060 || cp === 0x180E) {
|
|
165
|
+
count++;
|
|
166
|
+
}
|
|
167
|
+
// BOM only suspicious after position 0
|
|
168
|
+
else if (cp === 0xFEFF && i > 0) {
|
|
169
|
+
count++;
|
|
170
|
+
}
|
|
171
|
+
// BMP variation selectors (U+FE00-U+FE0F)
|
|
172
|
+
else if (cp >= 0xFE00 && cp <= 0xFE0F) {
|
|
173
|
+
count++;
|
|
174
|
+
}
|
|
175
|
+
// Supplementary plane: variation selectors supplement (U+E0100-U+E01EF)
|
|
176
|
+
else if (cp >= 0xE0100 && cp <= 0xE01EF) {
|
|
177
|
+
count++;
|
|
178
|
+
i++; // skip surrogate pair low half
|
|
179
|
+
}
|
|
180
|
+
// Supplementary plane: tag characters (U+E0001-U+E007F)
|
|
181
|
+
else if (cp >= 0xE0001 && cp <= 0xE007F) {
|
|
182
|
+
count++;
|
|
183
|
+
i++; // skip surrogate pair low half
|
|
184
|
+
}
|
|
185
|
+
// Skip surrogate pair low half for other supplementary chars
|
|
186
|
+
else if (cp > 0xFFFF) {
|
|
187
|
+
i++;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
return count;
|
|
191
|
+
}
|
|
192
|
+
|
|
133
193
|
module.exports = { detectObfuscation };
|
package/src/scanner/package.js
CHANGED
|
@@ -28,6 +28,13 @@ const DANGEROUS_PATTERNS = [
|
|
|
28
28
|
const DANGEROUS_KEYS = new Set(['__proto__', 'constructor', 'prototype', 'toString', 'valueOf']);
|
|
29
29
|
const DEP_FP_WHITELIST = new Set(['es5-ext', 'bootstrap-sass']);
|
|
30
30
|
|
|
31
|
+
// System commands that should never be shadowed via the "bin" field (PATH hijack)
|
|
32
|
+
const SHADOWED_COMMANDS = new Set([
|
|
33
|
+
'node', 'npm', 'npx', 'git', 'sh', 'bash', 'zsh', 'python', 'python3',
|
|
34
|
+
'curl', 'wget', 'ssh', 'scp', 'tar', 'make', 'gcc', 'go', 'ruby',
|
|
35
|
+
'perl', 'php', 'java', 'javac', 'pip', 'pip3', 'yarn', 'pnpm', 'bun'
|
|
36
|
+
]);
|
|
37
|
+
|
|
31
38
|
/**
|
|
32
39
|
* Clean a version specifier to extract the primary version number.
|
|
33
40
|
* Handles: ^1.0.0, ~1.0.0, >=1.0.0, >=1.0.0,<2.0.0, git URLs, etc.
|
|
@@ -95,6 +102,16 @@ async function scanPackageJson(targetPath) {
|
|
|
95
102
|
});
|
|
96
103
|
}
|
|
97
104
|
}
|
|
105
|
+
|
|
106
|
+
// Detect Bun runtime evasion in lifecycle scripts (Shai-Hulud 2.0)
|
|
107
|
+
if (/\bbun\s+(run|exec|install|x)\b/.test(scriptContent) || /\bbunx\s+/.test(scriptContent)) {
|
|
108
|
+
threats.push({
|
|
109
|
+
type: 'bun_runtime_evasion',
|
|
110
|
+
severity: 'HIGH',
|
|
111
|
+
message: `Bun runtime invocation in lifecycle script "${scriptName}" — alternative runtime to evade Node.js monitoring/sandboxing.`,
|
|
112
|
+
file: 'package.json'
|
|
113
|
+
});
|
|
114
|
+
}
|
|
98
115
|
}
|
|
99
116
|
}
|
|
100
117
|
|
|
@@ -113,6 +130,39 @@ async function scanPackageJson(targetPath) {
|
|
|
113
130
|
}
|
|
114
131
|
}
|
|
115
132
|
|
|
133
|
+
// Detect bin field hijacking: shadowing system commands (node, npm, git, bash, etc.)
|
|
134
|
+
if (pkg.bin) {
|
|
135
|
+
const binEntries = typeof pkg.bin === 'string'
|
|
136
|
+
? { [pkg.name]: pkg.bin }
|
|
137
|
+
: pkg.bin;
|
|
138
|
+
for (const [cmdName, cmdPath] of Object.entries(binEntries || {})) {
|
|
139
|
+
if (SHADOWED_COMMANDS.has(cmdName)) {
|
|
140
|
+
threats.push({
|
|
141
|
+
type: 'bin_field_hijack',
|
|
142
|
+
severity: 'CRITICAL',
|
|
143
|
+
message: `package.json "bin" field shadows system command "${cmdName}" → ${cmdPath}. PATH hijack: all npm scripts will execute this instead of the real ${cmdName}.`,
|
|
144
|
+
file: 'package.json'
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Detect .npmrc with git= override (PackageGate technique)
|
|
151
|
+
const npmrcPath = path.join(targetPath, '.npmrc');
|
|
152
|
+
if (fs.existsSync(npmrcPath)) {
|
|
153
|
+
try {
|
|
154
|
+
const npmrcContent = fs.readFileSync(npmrcPath, 'utf8');
|
|
155
|
+
if (/^git\s*=/m.test(npmrcContent)) {
|
|
156
|
+
threats.push({
|
|
157
|
+
type: 'npmrc_git_override',
|
|
158
|
+
severity: 'CRITICAL',
|
|
159
|
+
message: '.npmrc contains git= override — PackageGate technique: replaces git binary with attacker-controlled script.',
|
|
160
|
+
file: '.npmrc'
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
} catch { /* permission error */ }
|
|
164
|
+
}
|
|
165
|
+
|
|
116
166
|
// Scan declared dependencies against IOCs
|
|
117
167
|
let iocs;
|
|
118
168
|
try {
|
|
@@ -156,12 +206,21 @@ async function scanPackageJson(targetPath) {
|
|
|
156
206
|
].some(p => p.test(urlLower));
|
|
157
207
|
threats.push({
|
|
158
208
|
type: 'dependency_url_suspicious',
|
|
159
|
-
severity: isSuspicious ? '
|
|
209
|
+
severity: isSuspicious ? 'CRITICAL' : 'HIGH',
|
|
160
210
|
message: `Dependency "${depName}" uses HTTP URL: ${depVersion}` +
|
|
161
211
|
(isSuspicious ? ' (tunnel/private/localhost)' : ' (unusual, verify source)'),
|
|
162
212
|
file: 'package.json'
|
|
163
213
|
});
|
|
164
214
|
}
|
|
215
|
+
// Detect git-based dependencies — potential PackageGate RCE vector
|
|
216
|
+
if (typeof depVersion === 'string' && /^git[+:]/.test(depVersion)) {
|
|
217
|
+
threats.push({
|
|
218
|
+
type: 'git_dependency_rce',
|
|
219
|
+
severity: 'HIGH',
|
|
220
|
+
message: `Dependency "${depName}" uses git URL: ${depVersion} — potential PackageGate RCE vector (malicious .npmrc can override git binary).`,
|
|
221
|
+
file: 'package.json'
|
|
222
|
+
});
|
|
223
|
+
}
|
|
165
224
|
// Skip known FP packages that share names with malicious IOC entries
|
|
166
225
|
if (DEP_FP_WHITELIST.has(depName)) continue;
|
|
167
226
|
let malicious = null;
|
package/src/scanner/shell.js
CHANGED
|
@@ -24,7 +24,9 @@ const MALICIOUS_PATTERNS = [
|
|
|
24
24
|
// IFS evasion patterns (v2.6.9)
|
|
25
25
|
{ pattern: /curl\$\{?IFS\}?.*\|.*sh/m, name: 'curl_ifs_evasion', severity: 'CRITICAL' },
|
|
26
26
|
{ pattern: /eval\s+.*\$\(curl/m, name: 'eval_curl_subshell', severity: 'CRITICAL' },
|
|
27
|
-
{ pattern: /sh\s+-c\s+['"].*curl/m, name: 'sh_c_curl_exec', severity: 'HIGH' }
|
|
27
|
+
{ pattern: /sh\s+-c\s+['"].*curl/m, name: 'sh_c_curl_exec', severity: 'HIGH' },
|
|
28
|
+
// Bun runtime evasion (v2.8.9 — Shai-Hulud 2.0)
|
|
29
|
+
{ pattern: /\bbun\s+run\b/m, name: 'bun_runtime_evasion', severity: 'HIGH' }
|
|
28
30
|
];
|
|
29
31
|
|
|
30
32
|
const SHEBANG_RE = /^#!.*\b(?:ba)?sh\b/;
|
package/src/scoring.js
CHANGED
|
@@ -154,7 +154,9 @@ const DIST_EXEMPT_TYPES = new Set([
|
|
|
154
154
|
'cross_file_dataflow', // credential read → network exfil across files
|
|
155
155
|
'staged_eval_decode', // eval(atob(...)) (explicit payload staging)
|
|
156
156
|
'reverse_shell', // net.Socket + connect + pipe (always malicious)
|
|
157
|
-
'detached_credential_exfil' // detached process + credential exfil (DPRK/Lazarus)
|
|
157
|
+
'detached_credential_exfil', // detached process + credential exfil (DPRK/Lazarus)
|
|
158
|
+
'node_modules_write', // writeFile to node_modules/ (worm propagation)
|
|
159
|
+
'npm_publish_worm' // exec("npm publish") (worm propagation)
|
|
158
160
|
// P6: remote_code_load and proxy_data_intercept removed — in bundled dist/ files,
|
|
159
161
|
// fetch + eval co-occurrence is coincidental (bundler combines HTTP client + template compilation).
|
|
160
162
|
// fetch_decrypt_exec (fetch+decrypt+eval triple) remains exempt — never coincidental.
|