muaddib-scanner 2.9.0 → 2.9.2
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/index.js +6 -1
- package/src/response/playbooks.js +51 -0
- package/src/rules/index.js +125 -0
- package/src/scanner/ast-detectors.js +96 -1
- package/src/scanner/ast.js +7 -1
- package/src/scanner/obfuscation.js +60 -0
- package/src/scoring.js +97 -3
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
package/src/index.js
CHANGED
|
@@ -28,7 +28,7 @@ const { computeReachableFiles } = require('./scanner/reachability.js');
|
|
|
28
28
|
const { runTemporalAnalyses } = require('./temporal-runner.js');
|
|
29
29
|
const { formatOutput } = require('./output-formatter.js');
|
|
30
30
|
const { setExtraExcludes, getExtraExcludes, Spinner, listInstalledPackages, clearFileListCache, debugLog } = require('./utils.js');
|
|
31
|
-
const { SEVERITY_WEIGHTS, RISK_THRESHOLDS, MAX_RISK_SCORE, isPackageLevelThreat, computeGroupScore, applyFPReductions, calculateRiskScore } = require('./scoring.js');
|
|
31
|
+
const { SEVERITY_WEIGHTS, RISK_THRESHOLDS, MAX_RISK_SCORE, isPackageLevelThreat, computeGroupScore, applyFPReductions, applyCompoundBoosts, calculateRiskScore } = require('./scoring.js');
|
|
32
32
|
const { buildIntentPairs } = require('./intent-graph.js');
|
|
33
33
|
|
|
34
34
|
const { MAX_FILE_SIZE, safeParse } = require('./shared/constants.js');
|
|
@@ -598,6 +598,11 @@ async function run(targetPath, options = {}) {
|
|
|
598
598
|
// A malware package typically has 1-3 occurrences, not dozens.
|
|
599
599
|
applyFPReductions(deduped, reachableFiles, packageName, packageDeps);
|
|
600
600
|
|
|
601
|
+
// Compound scoring: inject synthetic CRITICAL threats when co-occurring types
|
|
602
|
+
// indicate unambiguous malice. Applied AFTER FP reductions to recover signals
|
|
603
|
+
// that were individually downgraded (count-based, dist, reachability).
|
|
604
|
+
applyCompoundBoosts(deduped);
|
|
605
|
+
|
|
601
606
|
// Intent coherence analysis: detect source→sink pairs within files
|
|
602
607
|
// Pass targetPath for destination-aware SDK pattern detection
|
|
603
608
|
const intentResult = buildIntentPairs(deduped, targetPath);
|
|
@@ -515,6 +515,57 @@ const PLAYBOOKS = {
|
|
|
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
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
|
+
crypto_staged_payload:
|
|
541
|
+
'CRITIQUE: Chaine steganographique complete detectee — fichier binaire (.png/.jpg/.wasm) avec eval() + dechiffrement crypto. ' +
|
|
542
|
+
'Le payload malveillant est cache dans un fichier binaire et dechiffre a runtime. Supprimer le package immediatement. ' +
|
|
543
|
+
'Analyser le fichier binaire dans un sandbox pour extraire le payload.',
|
|
544
|
+
|
|
545
|
+
lifecycle_typosquat:
|
|
546
|
+
'CRITIQUE: Package avec nom similaire a un package populaire ET scripts lifecycle. ' +
|
|
547
|
+
'Vecteur classique de dependency confusion: le code s\'execute a l\'installation. ' +
|
|
548
|
+
'NE PAS installer. Verifier le nom exact du package. Signaler sur npm.',
|
|
549
|
+
|
|
550
|
+
credential_env_exfil:
|
|
551
|
+
'CRITIQUE: Ecriture dans des chemins sensibles (cache npm/yarn, credentials) + acces aux variables d\'environnement. ' +
|
|
552
|
+
'Double vecteur d\'exfiltration de credentials. Supprimer le package. Regenerer tous les secrets. ' +
|
|
553
|
+
'Nettoyer le cache: npm cache clean --force.',
|
|
554
|
+
|
|
555
|
+
lifecycle_inline_exec:
|
|
556
|
+
'CRITIQUE: Script lifecycle avec node -e (execution inline). Le code s\'execute automatiquement a npm install. ' +
|
|
557
|
+
'NE PAS installer. Si deja installe: considerer la machine compromise. ' +
|
|
558
|
+
'Auditer les modifications systeme recentes.',
|
|
559
|
+
|
|
560
|
+
lifecycle_remote_require:
|
|
561
|
+
'CRITIQUE: Script lifecycle avec require(http/https) pour charger du code distant. ' +
|
|
562
|
+
'Le payload est telecharge et execute automatiquement a l\'installation. ' +
|
|
563
|
+
'NE PAS installer. Bloquer les connexions sortantes. Supprimer le package.',
|
|
564
|
+
|
|
565
|
+
obfuscated_credential_tampering:
|
|
566
|
+
'CRITIQUE: Code obfusque + ecriture dans des chemins sensibles. Dissimulation de vol de credentials. ' +
|
|
567
|
+
'Supprimer le package immediatement. Nettoyer le cache npm/yarn. Regenerer tous les secrets.',
|
|
568
|
+
|
|
518
569
|
bin_field_hijack:
|
|
519
570
|
'CRITIQUE: Le champ "bin" de package.json shadow une commande systeme (node, npm, git, bash, etc.). ' +
|
|
520
571
|
'A l\'installation, npm cree un symlink dans node_modules/.bin/ qui intercepte la commande reelle. ' +
|
package/src/rules/index.js
CHANGED
|
@@ -1544,6 +1544,131 @@ const RULES = {
|
|
|
1544
1544
|
],
|
|
1545
1545
|
mitre: 'T1059'
|
|
1546
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
|
+
},
|
|
1597
|
+
|
|
1598
|
+
// Compound scoring rules (v2.9.2)
|
|
1599
|
+
// Injected by applyCompoundBoosts() when co-occurring threat types indicate unambiguous malice.
|
|
1600
|
+
crypto_staged_payload: {
|
|
1601
|
+
id: 'MUADDIB-COMPOUND-001',
|
|
1602
|
+
name: 'Steganographic Payload + Crypto Decryption',
|
|
1603
|
+
severity: 'CRITICAL',
|
|
1604
|
+
confidence: 'high',
|
|
1605
|
+
description: 'Reference a un fichier binaire (.png/.jpg/.wasm) avec eval() combinee avec dechiffrement crypto (createDecipher). Chaine steganographique complete: payload cache dans un fichier binaire, dechiffre a runtime.',
|
|
1606
|
+
references: [
|
|
1607
|
+
'https://attack.mitre.org/techniques/T1140/',
|
|
1608
|
+
'https://attack.mitre.org/techniques/T1027/003/'
|
|
1609
|
+
],
|
|
1610
|
+
mitre: 'T1140'
|
|
1611
|
+
},
|
|
1612
|
+
lifecycle_typosquat: {
|
|
1613
|
+
id: 'MUADDIB-COMPOUND-002',
|
|
1614
|
+
name: 'Lifecycle Hook on Typosquat Package',
|
|
1615
|
+
severity: 'CRITICAL',
|
|
1616
|
+
confidence: 'high',
|
|
1617
|
+
description: 'Script lifecycle (preinstall/postinstall) sur un package avec nom similaire a un package populaire. Vecteur classique de dependency confusion: le code s\'execute automatiquement a l\'installation.',
|
|
1618
|
+
references: [
|
|
1619
|
+
'https://attack.mitre.org/techniques/T1195/002/',
|
|
1620
|
+
'https://snyk.io/blog/typosquatting-attacks/'
|
|
1621
|
+
],
|
|
1622
|
+
mitre: 'T1195.002'
|
|
1623
|
+
},
|
|
1624
|
+
credential_env_exfil: {
|
|
1625
|
+
id: 'MUADDIB-COMPOUND-003',
|
|
1626
|
+
name: 'Credential Tampering + Env Access',
|
|
1627
|
+
severity: 'CRITICAL',
|
|
1628
|
+
confidence: 'high',
|
|
1629
|
+
description: 'Ecriture dans un chemin sensible (cache npm/yarn, credentials) combinee avec acces aux variables d\'environnement. Chaine d\'exfiltration de credentials par double vecteur.',
|
|
1630
|
+
references: [
|
|
1631
|
+
'https://attack.mitre.org/techniques/T1552/001/',
|
|
1632
|
+
'https://attack.mitre.org/techniques/T1565/001/'
|
|
1633
|
+
],
|
|
1634
|
+
mitre: 'T1552.001'
|
|
1635
|
+
},
|
|
1636
|
+
lifecycle_inline_exec: {
|
|
1637
|
+
id: 'MUADDIB-COMPOUND-004',
|
|
1638
|
+
name: 'Lifecycle Hook + Inline Node Execution',
|
|
1639
|
+
severity: 'CRITICAL',
|
|
1640
|
+
confidence: 'high',
|
|
1641
|
+
description: 'Script lifecycle avec execution inline Node.js (node -e). Le code s\'execute automatiquement a npm install avec un payload inline.',
|
|
1642
|
+
references: [
|
|
1643
|
+
'https://attack.mitre.org/techniques/T1059/007/',
|
|
1644
|
+
'https://attack.mitre.org/techniques/T1195/002/'
|
|
1645
|
+
],
|
|
1646
|
+
mitre: 'T1059.007'
|
|
1647
|
+
},
|
|
1648
|
+
lifecycle_remote_require: {
|
|
1649
|
+
id: 'MUADDIB-COMPOUND-005',
|
|
1650
|
+
name: 'Lifecycle Hook + Remote Code Loading',
|
|
1651
|
+
severity: 'CRITICAL',
|
|
1652
|
+
confidence: 'high',
|
|
1653
|
+
description: 'Script lifecycle avec require(http/https) pour charger du code distant. Le payload est telecharge et execute automatiquement a l\'installation.',
|
|
1654
|
+
references: [
|
|
1655
|
+
'https://attack.mitre.org/techniques/T1105/',
|
|
1656
|
+
'https://attack.mitre.org/techniques/T1195/002/'
|
|
1657
|
+
],
|
|
1658
|
+
mitre: 'T1105'
|
|
1659
|
+
},
|
|
1660
|
+
obfuscated_credential_tampering: {
|
|
1661
|
+
id: 'MUADDIB-COMPOUND-006',
|
|
1662
|
+
name: 'Obfuscated Code + Credential Tampering',
|
|
1663
|
+
severity: 'CRITICAL',
|
|
1664
|
+
confidence: 'high',
|
|
1665
|
+
description: 'Code obfusque combine avec ecriture dans des chemins sensibles (cache npm/yarn, credentials). Dissimulation de vol de credentials.',
|
|
1666
|
+
references: [
|
|
1667
|
+
'https://attack.mitre.org/techniques/T1027/',
|
|
1668
|
+
'https://attack.mitre.org/techniques/T1565/001/'
|
|
1669
|
+
],
|
|
1670
|
+
mitre: 'T1027'
|
|
1671
|
+
},
|
|
1547
1672
|
};
|
|
1548
1673
|
|
|
1549
1674
|
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)
|
|
@@ -1653,6 +1682,10 @@ function handleImportExpression(node, ctx) {
|
|
|
1653
1682
|
file: ctx.relFile
|
|
1654
1683
|
});
|
|
1655
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
|
+
}
|
|
1656
1689
|
} else {
|
|
1657
1690
|
ctx.threats.push({
|
|
1658
1691
|
type: 'dynamic_import',
|
|
@@ -1821,6 +1854,34 @@ function handleLiteral(node, ctx) {
|
|
|
1821
1854
|
file: ctx.relFile
|
|
1822
1855
|
});
|
|
1823
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
|
+
}
|
|
1824
1885
|
}
|
|
1825
1886
|
}
|
|
1826
1887
|
|
|
@@ -1995,6 +2056,16 @@ function handleMemberExpression(node, ctx) {
|
|
|
1995
2056
|
});
|
|
1996
2057
|
}
|
|
1997
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
|
+
|
|
1998
2069
|
if (
|
|
1999
2070
|
node.object?.object?.name === 'process' &&
|
|
2000
2071
|
node.object?.property?.name === 'env'
|
|
@@ -2299,6 +2370,30 @@ function handlePostWalk(ctx) {
|
|
|
2299
2370
|
file: ctx.relFile
|
|
2300
2371
|
});
|
|
2301
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
|
+
}
|
|
2302
2397
|
}
|
|
2303
2398
|
|
|
2304
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/scoring.js
CHANGED
|
@@ -62,7 +62,9 @@ const PACKAGE_LEVEL_TYPES = new Set([
|
|
|
62
62
|
'publish_burst', 'publish_dormant_spike', 'publish_rapid_succession',
|
|
63
63
|
'maintainer_new_suspicious', 'maintainer_sole_change',
|
|
64
64
|
'sandbox_network_activity', 'sandbox_file_changes', 'sandbox_process_spawns',
|
|
65
|
-
'sandbox_canary_exfiltration'
|
|
65
|
+
'sandbox_canary_exfiltration',
|
|
66
|
+
// Compound scoring rules — package-level co-occurrences
|
|
67
|
+
'lifecycle_typosquat', 'lifecycle_inline_exec', 'lifecycle_remote_require'
|
|
66
68
|
]);
|
|
67
69
|
|
|
68
70
|
/**
|
|
@@ -156,7 +158,12 @@ const DIST_EXEMPT_TYPES = new Set([
|
|
|
156
158
|
'reverse_shell', // net.Socket + connect + pipe (always malicious)
|
|
157
159
|
'detached_credential_exfil', // detached process + credential exfil (DPRK/Lazarus)
|
|
158
160
|
'node_modules_write', // writeFile to node_modules/ (worm propagation)
|
|
159
|
-
'npm_publish_worm'
|
|
161
|
+
'npm_publish_worm', // exec("npm publish") (worm propagation)
|
|
162
|
+
// Dangerous shell commands in dist/ are real threats, never bundler output
|
|
163
|
+
'dangerous_exec',
|
|
164
|
+
// Compound scoring rules — co-occurrence signals, never FP
|
|
165
|
+
'crypto_staged_payload', 'lifecycle_typosquat', 'credential_env_exfil',
|
|
166
|
+
'lifecycle_inline_exec', 'lifecycle_remote_require', 'obfuscated_credential_tampering'
|
|
160
167
|
// P6: remote_code_load and proxy_data_intercept removed — in bundled dist/ files,
|
|
161
168
|
// fetch + eval co-occurrence is coincidental (bundler combines HTTP client + template compilation).
|
|
162
169
|
// fetch_decrypt_exec (fetch+decrypt+eval triple) remains exempt — never coincidental.
|
|
@@ -203,6 +210,93 @@ const REACHABILITY_EXEMPT_TYPES = new Set([
|
|
|
203
210
|
'detached_credential_exfil' // DPRK/Lazarus: invoked via lifecycle, not require/import
|
|
204
211
|
]);
|
|
205
212
|
|
|
213
|
+
// ============================================
|
|
214
|
+
// COMPOUND SCORING RULES (v2.9.2)
|
|
215
|
+
// ============================================
|
|
216
|
+
// Co-occurrences of threat types that NEVER appear in benign packages.
|
|
217
|
+
// Applied AFTER FP reductions to recover signals that were individually downgraded.
|
|
218
|
+
// Each compound injects a new CRITICAL threat when all required types are present.
|
|
219
|
+
const SCORING_COMPOUNDS = [
|
|
220
|
+
{
|
|
221
|
+
type: 'crypto_staged_payload',
|
|
222
|
+
requires: ['staged_binary_payload', 'crypto_decipher'],
|
|
223
|
+
severity: 'CRITICAL',
|
|
224
|
+
message: 'Binary file reference + crypto decryption — steganographic payload chain (scoring compound).',
|
|
225
|
+
fileFrom: 'staged_binary_payload'
|
|
226
|
+
},
|
|
227
|
+
{
|
|
228
|
+
type: 'lifecycle_typosquat',
|
|
229
|
+
requires: ['lifecycle_script', 'typosquat_detected'],
|
|
230
|
+
severity: 'CRITICAL',
|
|
231
|
+
message: 'Lifecycle hook on typosquat package — dependency confusion attack vector (scoring compound).',
|
|
232
|
+
fileFrom: 'typosquat_detected'
|
|
233
|
+
},
|
|
234
|
+
{
|
|
235
|
+
type: 'credential_env_exfil',
|
|
236
|
+
requires: ['credential_tampering', 'env_access'],
|
|
237
|
+
severity: 'CRITICAL',
|
|
238
|
+
message: 'Credential path tampering + environment variable access — credential exfiltration chain (scoring compound).',
|
|
239
|
+
fileFrom: 'credential_tampering'
|
|
240
|
+
},
|
|
241
|
+
{
|
|
242
|
+
type: 'lifecycle_inline_exec',
|
|
243
|
+
requires: ['lifecycle_script', 'node_inline_exec'],
|
|
244
|
+
severity: 'CRITICAL',
|
|
245
|
+
message: 'Lifecycle hook with inline Node execution (node -e) — install-time code execution (scoring compound).',
|
|
246
|
+
fileFrom: 'node_inline_exec'
|
|
247
|
+
},
|
|
248
|
+
{
|
|
249
|
+
type: 'lifecycle_remote_require',
|
|
250
|
+
requires: ['lifecycle_script', 'network_require'],
|
|
251
|
+
severity: 'CRITICAL',
|
|
252
|
+
message: 'Lifecycle hook loading remote code (require http/https) — supply chain payload delivery (scoring compound).',
|
|
253
|
+
fileFrom: 'network_require'
|
|
254
|
+
},
|
|
255
|
+
{
|
|
256
|
+
type: 'obfuscated_credential_tampering',
|
|
257
|
+
requires: ['credential_tampering', 'obfuscation_detected'],
|
|
258
|
+
severity: 'CRITICAL',
|
|
259
|
+
message: 'Obfuscated code + credential path tampering — concealed credential theft (scoring compound).',
|
|
260
|
+
fileFrom: 'credential_tampering'
|
|
261
|
+
}
|
|
262
|
+
];
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Apply compound boost rules: inject synthetic CRITICAL threats when
|
|
266
|
+
* co-occurring threat types indicate unambiguous malice.
|
|
267
|
+
* Called AFTER applyFPReductions to recover individually-downgraded signals.
|
|
268
|
+
* @param {Array} threats - deduplicated threat array (mutated in place)
|
|
269
|
+
*/
|
|
270
|
+
function applyCompoundBoosts(threats) {
|
|
271
|
+
const typeSet = new Set(threats.map(t => t.type));
|
|
272
|
+
|
|
273
|
+
// Build map of type → first file encountered (for file assignment)
|
|
274
|
+
const typeFileMap = Object.create(null);
|
|
275
|
+
for (const t of threats) {
|
|
276
|
+
if (!typeFileMap[t.type]) {
|
|
277
|
+
typeFileMap[t.type] = t.file || '(unknown)';
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
for (const compound of SCORING_COMPOUNDS) {
|
|
282
|
+
// Skip if compound already present (e.g. from a scanner)
|
|
283
|
+
if (typeSet.has(compound.type)) continue;
|
|
284
|
+
|
|
285
|
+
// Check all required types are present
|
|
286
|
+
if (compound.requires.every(req => typeSet.has(req))) {
|
|
287
|
+
threats.push({
|
|
288
|
+
type: compound.type,
|
|
289
|
+
severity: compound.severity,
|
|
290
|
+
message: compound.message,
|
|
291
|
+
file: typeFileMap[compound.fileFrom] || '(unknown)',
|
|
292
|
+
count: 1,
|
|
293
|
+
compound: true
|
|
294
|
+
});
|
|
295
|
+
typeSet.add(compound.type);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
206
300
|
// Custom class prototypes that HTTP frameworks legitimately extend.
|
|
207
301
|
// Distinguished from dangerous core Node.js prototype hooks.
|
|
208
302
|
const FRAMEWORK_PROTOTYPES = ['Request', 'Response', 'App', 'Router'];
|
|
@@ -463,5 +557,5 @@ function calculateRiskScore(deduped, intentResult) {
|
|
|
463
557
|
|
|
464
558
|
module.exports = {
|
|
465
559
|
SEVERITY_WEIGHTS, RISK_THRESHOLDS, MAX_RISK_SCORE, CONFIDENCE_FACTORS,
|
|
466
|
-
isPackageLevelThreat, computeGroupScore, applyFPReductions, calculateRiskScore
|
|
560
|
+
isPackageLevelThreat, computeGroupScore, applyFPReductions, applyCompoundBoosts, calculateRiskScore
|
|
467
561
|
};
|