muaddib-scanner 2.10.22 → 2.10.23

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/bin/muaddib.js CHANGED
@@ -1,4 +1,26 @@
1
1
  #!/usr/bin/env node
2
+
3
+ // Auto-respawn with memory flags for evaluate command (OOM prevention)
4
+ if (process.argv[2] === 'evaluate') {
5
+ const hasMaxOld = process.execArgv.some(a => a.includes('--max-old-space-size'));
6
+ const hasGC = process.execArgv.some(a => a === '--expose-gc');
7
+ if (!hasMaxOld || !hasGC) {
8
+ const { execFileSync } = require('child_process');
9
+ const flags = [];
10
+ if (!hasMaxOld) flags.push('--max-old-space-size=8192');
11
+ if (!hasGC) flags.push('--expose-gc');
12
+ try {
13
+ execFileSync(process.execPath, [...flags, __filename, ...process.argv.slice(2)], {
14
+ stdio: 'inherit',
15
+ env: process.env
16
+ });
17
+ process.exit(0);
18
+ } catch (e) {
19
+ process.exit(e.status || 1);
20
+ }
21
+ }
22
+ }
23
+
2
24
  const { execFile } = require('child_process');
3
25
  const { run } = require('../src/index.js');
4
26
  const { updateIOCs } = require('../src/ioc/updater.js');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "muaddib-scanner",
3
- "version": "2.10.22",
3
+ "version": "2.10.23",
4
4
  "description": "Supply-chain threat detection & response for npm & PyPI/Python",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -740,6 +740,76 @@ const PLAYBOOKS = {
740
740
  'CRITIQUE: require("process").mainModule.require() detecte — contournement de la detection ' +
741
741
  'process.mainModule.require() via require("process") au lieu de l\'objet global. ' +
742
742
  'Aucun package legitime n\'utilise ce pattern. Supprimer immediatement.',
743
+
744
+ // Blue Team v8 — New playbook entries
745
+ shared_memory_ipc:
746
+ 'SharedArrayBuffer + Worker Thread detectes. Canal IPC memoire partagee qui contourne la surveillance. ' +
747
+ 'Verifier si les workers manipulent des donnees sensibles. Isoler si combine avec eval/exec.',
748
+
749
+ websocket_c2:
750
+ 'HAUTE: Connexion WebSocket vers domaine suspect ou avec execution dynamique. Canal C2 persistant bidirectionnel. ' +
751
+ 'Analyser l\'URL de connexion. Bloquer les connexions WebSocket sortantes. Verifier les messages echanges.',
752
+
753
+ udp_exfiltration:
754
+ 'HAUTE: Socket UDP (dgram) avec envoi de donnees. Exfiltration contournant les firewalls HTTP. ' +
755
+ 'Verifier les destinations IP. Bloquer le trafic UDP sortant non-DNS. Auditer les donnees envoyees.',
756
+
757
+ native_addon_install:
758
+ 'HAUTE: binding.gyp avec script lifecycle non-standard. Code natif compile a l\'installation. ' +
759
+ 'Verifier le contenu de binding.gyp et les sources C/C++. Installer avec --ignore-scripts si suspect.',
760
+
761
+ string_mutation_obfuscation:
762
+ 'HAUTE: Chaine de .replace() reconstruisant des noms d\'API dangereuses (leet-speak). ' +
763
+ 'Technique d\'evasion par substitution de caracteres. Decoder la chaine finale. Supprimer si malveillant.',
764
+
765
+ crontab_systemd_write:
766
+ 'CRITIQUE: Ecriture dans les fichiers cron/crontab. Persistence via tache planifiee. ' +
767
+ 'Verifier /etc/cron*, /var/spool/cron. Supprimer les entrees ajoutees. Auditer crontab -l.',
768
+
769
+ isolated_suspicious_file:
770
+ 'Un seul fichier suspect parmi de nombreux fichiers propres. Pattern de dissimulation typique ' +
771
+ 'ou le code malveillant est cache dans un package apparemment legitime. Examiner le fichier suspect en detail.',
772
+
773
+ deep_suspicious_file:
774
+ 'Pattern suspect dans un fichier profondement imbrique. Technique pour echapper aux analyses superficielles. ' +
775
+ 'Verifier le contenu du fichier et son role dans l\'arborescence du package.',
776
+
777
+ // Blue Team v8b playbooks
778
+ module_internals_hijack:
779
+ 'CRITIQUE: Assignation a Module._resolveFilename/_compile/_extensions. Detournement du systeme de modules Node.js. ' +
780
+ 'Tous les require() sont interceptes. Supprimer le package immediatement. Auditer tous les modules charges apres installation.',
781
+
782
+ json_reviver_pollution:
783
+ 'HAUTE: JSON.parse avec reviver accedant a __proto__/prototype. Pollution de prototype via JSON. ' +
784
+ 'Ne jamais passer de JSON non fiable avec un reviver manipulant __proto__. Verifier les sources de donnees JSON.',
785
+
786
+ vm_dynamic_code:
787
+ 'CRITIQUE: vm.runInContext/compileFunction avec code construit dynamiquement. Evasion de sandbox. ' +
788
+ 'Verifier d\'ou provient le code execute. Bloquer l\'acces au module vm. Supprimer si non justifie.',
789
+
790
+ callback_exec_rce:
791
+ 'CRITIQUE: exec/spawn dans callback .on(\'message\'|\'data\'). Execution de commandes depuis flux reseau. ' +
792
+ 'Pattern C2: commandes recues par WebSocket/TCP executees via child_process. Supprimer immediatement.',
793
+
794
+ stego_binary_exec:
795
+ 'CRITIQUE: Lecture de fichier image/binaire + eval/Function. Payload steganographique. ' +
796
+ 'Verifier le contenu du fichier image. Scanner avec des outils stego. Supprimer le fichier et le code d\'extraction.',
797
+
798
+ asynclocal_context_exec:
799
+ 'HAUTE: AsyncLocalStorage + execution dynamique. Code malveillant cache dans un contexte async. ' +
800
+ 'Examiner les callbacks AsyncLocalStorage. Verifier ce qui est stocke et execute dans le store.',
801
+
802
+ prototype_chain_constructor:
803
+ 'CRITIQUE: Object.getPrototypeOf(var).constructor stocke dans une variable. Traversee de prototype pour Function. ' +
804
+ 'Technique d\'evasion avancee. Supprimer le code. Verifier si le constructeur est appele avec du code dynamique.',
805
+
806
+ ci_environment_probe:
807
+ 'HAUTE: Detection de 3+ fournisseurs CI (GitHub Actions, GitLab CI, etc.) dans le meme fichier. ' +
808
+ 'Sondage d\'environnement CI pour activation conditionnelle. Verifier la logique conditionnelle associee.',
809
+
810
+ lifecycle_missing_script:
811
+ 'CRITIQUE: Script lifecycle reference un fichier inexistant dans le package. Script fantome. ' +
812
+ 'Le payload peut etre injecte dynamiquement ou lors d\'une mise a jour. Installer avec --ignore-scripts. Supprimer le package.',
743
813
  };
744
814
 
745
815
  function getPlaybook(threatType) {
@@ -1948,6 +1948,212 @@ const RULES = {
1948
1948
  ],
1949
1949
  mitre: 'T1059'
1950
1950
  },
1951
+
1952
+ // Blue Team v8 — New detections (AST-070 to AST-077, SHELL-023, SCORE-001/002)
1953
+ shared_memory_ipc: {
1954
+ id: 'MUADDIB-AST-070',
1955
+ name: 'Shared Memory IPC',
1956
+ severity: 'MEDIUM',
1957
+ confidence: 'medium',
1958
+ description: 'SharedArrayBuffer + Worker Thread detectes — canal IPC memoire partagee qui contourne la surveillance des messages inter-threads.',
1959
+ references: [
1960
+ 'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/SharedArrayBuffer',
1961
+ 'https://attack.mitre.org/techniques/T1559/'
1962
+ ],
1963
+ mitre: 'T1559'
1964
+ },
1965
+ websocket_c2: {
1966
+ id: 'MUADDIB-AST-071',
1967
+ name: 'WebSocket C2 Channel',
1968
+ severity: 'HIGH',
1969
+ confidence: 'high',
1970
+ description: 'Connexion WebSocket vers un domaine suspect ou avec execution dynamique — canal C2 bidirectionnel persistant.',
1971
+ references: [
1972
+ 'https://attack.mitre.org/techniques/T1071.001/',
1973
+ 'https://owasp.org/www-community/attacks/WebSocket_Hijacking'
1974
+ ],
1975
+ mitre: 'T1071.001'
1976
+ },
1977
+ udp_exfiltration: {
1978
+ id: 'MUADDIB-AST-072',
1979
+ name: 'UDP Data Exfiltration',
1980
+ severity: 'HIGH',
1981
+ confidence: 'high',
1982
+ description: 'Module dgram (UDP) avec envoi de donnees — exfiltration via protocole UDP qui contourne les firewalls HTTP.',
1983
+ references: [
1984
+ 'https://nodejs.org/api/dgram.html',
1985
+ 'https://attack.mitre.org/techniques/T1048.003/'
1986
+ ],
1987
+ mitre: 'T1048.003'
1988
+ },
1989
+ native_addon_install: {
1990
+ id: 'MUADDIB-AST-073',
1991
+ name: 'Native Addon Installation',
1992
+ severity: 'HIGH',
1993
+ confidence: 'medium',
1994
+ description: 'binding.gyp present avec script lifecycle non-standard — compilation native potentiellement malveillante a l\'installation.',
1995
+ references: [
1996
+ 'https://nodejs.org/api/addons.html',
1997
+ 'https://attack.mitre.org/techniques/T1195.002/'
1998
+ ],
1999
+ mitre: 'T1195.002'
2000
+ },
2001
+ string_mutation_obfuscation: {
2002
+ id: 'MUADDIB-AST-074',
2003
+ name: 'String Mutation Obfuscation',
2004
+ severity: 'HIGH',
2005
+ confidence: 'high',
2006
+ description: 'Chaine de 3+ appels .replace() pour reconstruire des noms d\'API dangereuses — technique leet-speak/substitution pour contourner la detection statique.',
2007
+ references: [
2008
+ 'https://attack.mitre.org/techniques/T1027/',
2009
+ 'https://attack.mitre.org/techniques/T1140/'
2010
+ ],
2011
+ mitre: 'T1027'
2012
+ },
2013
+ crontab_systemd_write: {
2014
+ id: 'MUADDIB-SHELL-023',
2015
+ name: 'Crontab/Cron Write',
2016
+ severity: 'CRITICAL',
2017
+ confidence: 'high',
2018
+ description: 'Ecriture dans les fichiers cron (/etc/cron*, crontab, /var/spool/cron) — persistence via tache planifiee.',
2019
+ references: [
2020
+ 'https://attack.mitre.org/techniques/T1053.003/',
2021
+ 'https://attack.mitre.org/techniques/T1543/'
2022
+ ],
2023
+ mitre: 'T1053.003'
2024
+ },
2025
+ isolated_suspicious_file: {
2026
+ id: 'MUADDIB-SCORE-001',
2027
+ name: 'Isolated Suspicious File',
2028
+ severity: 'MEDIUM',
2029
+ confidence: 'medium',
2030
+ description: 'Un seul fichier suspect parmi 10+ fichiers propres — pattern de dissimulation ou le code malveillant est cache dans un package legitime.',
2031
+ references: [
2032
+ 'https://attack.mitre.org/techniques/T1036/'
2033
+ ],
2034
+ mitre: 'T1036'
2035
+ },
2036
+ deep_suspicious_file: {
2037
+ id: 'MUADDIB-SCORE-002',
2038
+ name: 'Deeply Nested Suspicious File',
2039
+ severity: 'LOW',
2040
+ confidence: 'low',
2041
+ description: 'Pattern suspect detecte dans un fichier profondement imbrique (profondeur > 3) — technique de dissimulation dans l\'arborescence du package.',
2042
+ references: [
2043
+ 'https://attack.mitre.org/techniques/T1036.005/'
2044
+ ],
2045
+ mitre: 'T1036.005'
2046
+ },
2047
+
2048
+ // Blue Team v8b — New detections (AST-075 to AST-082, PKG-017)
2049
+ module_internals_hijack: {
2050
+ id: 'MUADDIB-AST-075',
2051
+ name: 'Module Internals Hijack',
2052
+ severity: 'CRITICAL',
2053
+ confidence: 'high',
2054
+ description: 'Assignation a Module._resolveFilename, _compile ou _extensions — detournement des mecanismes internes du systeme de modules Node.js. Tous les require() subsequents peuvent etre interceptes.',
2055
+ references: [
2056
+ 'https://nodejs.org/api/modules.html',
2057
+ 'https://attack.mitre.org/techniques/T1574.006/'
2058
+ ],
2059
+ mitre: 'T1574.006'
2060
+ },
2061
+ json_reviver_pollution: {
2062
+ id: 'MUADDIB-AST-076',
2063
+ name: 'JSON Reviver Prototype Pollution',
2064
+ severity: 'HIGH',
2065
+ confidence: 'high',
2066
+ description: 'JSON.parse avec fonction reviver accedant a __proto__ ou prototype — pollution de prototype via donnees JSON non fiables.',
2067
+ references: [
2068
+ 'https://portswigger.net/web-security/prototype-pollution',
2069
+ 'https://attack.mitre.org/techniques/T1059.007/'
2070
+ ],
2071
+ mitre: 'T1059.007'
2072
+ },
2073
+ vm_dynamic_code: {
2074
+ id: 'MUADDIB-AST-077',
2075
+ name: 'VM Dynamic Code Execution',
2076
+ severity: 'CRITICAL',
2077
+ confidence: 'high',
2078
+ description: 'vm.runInContext/runInNewContext/compileFunction avec code construit dynamiquement — evasion du sandbox via code genere au runtime.',
2079
+ references: [
2080
+ 'https://nodejs.org/api/vm.html',
2081
+ 'https://attack.mitre.org/techniques/T1059.007/'
2082
+ ],
2083
+ mitre: 'T1059.007'
2084
+ },
2085
+ callback_exec_rce: {
2086
+ id: 'MUADDIB-AST-078',
2087
+ name: 'Callback Remote Code Execution',
2088
+ severity: 'CRITICAL',
2089
+ confidence: 'high',
2090
+ description: 'exec/spawn dans un callback .on(\'message\') ou .on(\'data\') avec child_process — execution de commandes a distance depuis un flux reseau.',
2091
+ references: [
2092
+ 'https://attack.mitre.org/techniques/T1059/',
2093
+ 'https://attack.mitre.org/techniques/T1071/'
2094
+ ],
2095
+ mitre: 'T1059'
2096
+ },
2097
+ stego_binary_exec: {
2098
+ id: 'MUADDIB-AST-079',
2099
+ name: 'Steganographic Binary Execution',
2100
+ severity: 'CRITICAL',
2101
+ confidence: 'high',
2102
+ description: 'Lecture de fichier binaire/image (PNG, JPG) + execution dynamique (eval/Function) — extraction et execution de payload steganographique.',
2103
+ references: [
2104
+ 'https://attack.mitre.org/techniques/T1027.003/',
2105
+ 'https://attack.mitre.org/techniques/T1140/'
2106
+ ],
2107
+ mitre: 'T1027.003'
2108
+ },
2109
+ asynclocal_context_exec: {
2110
+ id: 'MUADDIB-AST-080',
2111
+ name: 'AsyncLocalStorage Context Execution',
2112
+ severity: 'HIGH',
2113
+ confidence: 'medium',
2114
+ description: 'AsyncLocalStorage + execution dynamique — code malveillant cache dans un contexte asynchrone, echappe a l\'analyse de pile d\'appels synchrone.',
2115
+ references: [
2116
+ 'https://nodejs.org/api/async_context.html',
2117
+ 'https://attack.mitre.org/techniques/T1059.007/'
2118
+ ],
2119
+ mitre: 'T1059.007'
2120
+ },
2121
+ prototype_chain_constructor: {
2122
+ id: 'MUADDIB-AST-081',
2123
+ name: 'Prototype Chain Constructor Access',
2124
+ severity: 'CRITICAL',
2125
+ confidence: 'high',
2126
+ description: 'Object.getPrototypeOf(variable).constructor extrait dans une variable — traversee de la chaine de prototypes pour atteindre le constructeur Function et executer du code arbitraire.',
2127
+ references: [
2128
+ 'https://attack.mitre.org/techniques/T1059.007/',
2129
+ 'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/getPrototypeOf'
2130
+ ],
2131
+ mitre: 'T1059.007'
2132
+ },
2133
+ ci_environment_probe: {
2134
+ id: 'MUADDIB-AST-082',
2135
+ name: 'CI Environment Fingerprinting',
2136
+ severity: 'HIGH',
2137
+ confidence: 'medium',
2138
+ description: 'References a 3+ variables d\'environnement de fournisseurs CI (GITHUB_ACTIONS, GITLAB_CI, etc.) — sondage d\'environnement CI pour activation conditionnelle de payload.',
2139
+ references: [
2140
+ 'https://attack.mitre.org/techniques/T1082/',
2141
+ 'https://attack.mitre.org/techniques/T1497/'
2142
+ ],
2143
+ mitre: 'T1082'
2144
+ },
2145
+ lifecycle_missing_script: {
2146
+ id: 'MUADDIB-PKG-017',
2147
+ name: 'Phantom Lifecycle Script',
2148
+ severity: 'CRITICAL',
2149
+ confidence: 'high',
2150
+ description: 'Script lifecycle (preinstall/install) reference un fichier qui n\'existe pas dans le package — script fantome, le payload peut etre injecte au moment de la publication.',
2151
+ references: [
2152
+ 'https://attack.mitre.org/techniques/T1195.002/',
2153
+ 'https://blog.npmjs.org/post/166316363605/the-lifecycle-script-vulnerability'
2154
+ ],
2155
+ mitre: 'T1195.002'
2156
+ },
1951
2157
  };
1952
2158
 
1953
2159
  function getRule(type) {
@@ -223,9 +223,14 @@ const BLOCKCHAIN_RPC_ENDPOINTS = [
223
223
  // Solana/Web3 C2 methods — used for dead drop resolver (GlassWorm)
224
224
  const SOLANA_C2_METHODS = [
225
225
  'getSignaturesForAddress', 'getAccountInfo', 'getTransaction',
226
- 'getConfirmedSignaturesForAddress2', 'getParsedTransaction'
226
+ 'getConfirmedSignaturesForAddress2', 'getParsedTransaction',
227
+ // Blue Team v8: extended Ethereum/Web3 C2 methods
228
+ 'eth_call', 'getCode', 'getLogs'
227
229
  ];
228
230
 
231
+ // Blue Team v8: Ethereum/Web3 package names for compound blockchain C2 detection
232
+ const ETHEREUM_PACKAGES = ['ethers', 'web3', '@ethersproject/providers', '@ethersproject/contracts'];
233
+
229
234
  // Solana/Web3 package names
230
235
  const SOLANA_PACKAGES = ['@solana/web3.js', 'solana-web3.js', '@solana/web3'];
231
236
 
@@ -531,6 +536,7 @@ function handleVariableDeclarator(node, ctx) {
531
536
  }
532
537
 
533
538
  // Audit v3 B3: const AF = Object.getPrototypeOf(async function(){}).constructor
539
+ // Blue Team v8: Extended to detect nested getPrototypeOf chains (2+ levels deep)
534
540
  if (node.init?.type === 'MemberExpression') {
535
541
  const initProp = node.init.computed
536
542
  ? (node.init.property?.type === 'Literal' ? String(node.init.property.value) : null)
@@ -557,6 +563,33 @@ function handleVariableDeclarator(node, ctx) {
557
563
  file: ctx.relFile
558
564
  });
559
565
  }
566
+ // Blue Team v8: Nested getPrototypeOf — Object.getPrototypeOf(Object.getPrototypeOf(...)).constructor
567
+ // Walking up prototype chain 2+ levels to reach Function constructor from any object
568
+ if (arg.type === 'CallExpression' && arg.callee?.type === 'MemberExpression' &&
569
+ arg.callee.property?.name === 'getPrototypeOf') {
570
+ ctx.evalAliases.set(node.id.name, 'Function');
571
+ ctx.hasDynamicExec = true;
572
+ ctx.threats.push({
573
+ type: 'dangerous_constructor',
574
+ severity: 'CRITICAL',
575
+ message: `Nested Object.getPrototypeOf() chain (2+ levels) + .constructor into "${node.id.name}" — deep prototype traversal to reach Function constructor.`,
576
+ file: ctx.relFile
577
+ });
578
+ }
579
+ // Blue Team v8b (A3): Object.getPrototypeOf(variable).constructor
580
+ // When a variable (possibly holding a prototype chain result) is passed to
581
+ // getPrototypeOf and .constructor is extracted — prototype chain traversal attack.
582
+ // Covers: const C = Object.getPrototypeOf(protoVar).constructor
583
+ if (arg.type === 'Identifier') {
584
+ ctx.evalAliases.set(node.id.name, 'Function');
585
+ ctx.hasDynamicExec = true;
586
+ ctx.threats.push({
587
+ type: 'prototype_chain_constructor',
588
+ severity: 'CRITICAL',
589
+ message: `Object.getPrototypeOf(${arg.name}).constructor extracted into "${node.id.name}" — prototype chain traversal to reach Function constructor.`,
590
+ file: ctx.relFile
591
+ });
592
+ }
560
593
  }
561
594
  }
562
595
  }
@@ -591,6 +624,21 @@ function handleVariableDeclarator(node, ctx) {
591
624
  ctx.stringVarValues.set(node.id.name, strVal);
592
625
  }
593
626
 
627
+ // Blue Team v8b (B7): Track path.join() results where last arg is an image/binary filename
628
+ // Enables steganographic payload detection when the variable is used with fs.readFileSync
629
+ if (!strVal && node.init?.type === 'CallExpression') {
630
+ const initCallName = getCallName(node.init);
631
+ if ((initCallName === 'join' || initCallName === 'resolve') &&
632
+ node.init.callee?.type === 'MemberExpression' &&
633
+ node.init.arguments?.length > 0) {
634
+ const lastArg = node.init.arguments[node.init.arguments.length - 1];
635
+ if (lastArg?.type === 'Literal' && typeof lastArg.value === 'string') {
636
+ // Store the last path component so image extension check works later
637
+ ctx.stringVarValues.set(node.id.name, lastArg.value);
638
+ }
639
+ }
640
+ }
641
+
594
642
  // Track variables assigned from require.cache[...] (module cache references)
595
643
  // Used to detect writes to cached module exports (require.cache poisoning)
596
644
  if (node.init?.type === 'MemberExpression' && node.init.computed) {
@@ -890,6 +938,14 @@ function handleCallExpression(node, ctx) {
890
938
  if (reqStr && SOLANA_PACKAGES.some(pkg => reqStr === pkg)) {
891
939
  ctx.hasSolanaImport = true;
892
940
  }
941
+ // Blue Team v8: track Ethereum/Web3 imports
942
+ if (reqStr && ETHEREUM_PACKAGES.some(pkg => reqStr === pkg || reqStr.startsWith(pkg))) {
943
+ ctx.hasSolanaImport = true; // reuse flag — both indicate blockchain SDK usage
944
+ }
945
+ // Blue Team v8: track require('ws') for WebSocket C2 detection
946
+ if (reqStr === 'ws' || reqStr === 'websocket') {
947
+ ctx.hasWebSocketNew = true; // ws module provides WebSocket functionality
948
+ }
893
949
  }
894
950
 
895
951
  // Detect process.mainModule.require('child_process') — module system bypass
@@ -1062,6 +1118,29 @@ function handleCallExpression(node, ctx) {
1062
1118
  }
1063
1119
  }
1064
1120
 
1121
+ // Blue Team v8b (C10): require('child_process').execSync/exec(variable) — inline require + variable command
1122
+ // When require is literal 'child_process' but the command argument is not resolvable (MemberExpression, Identifier),
1123
+ // this is a hidden exec with runtime-determined command (C2 pattern: cmd from network data)
1124
+ if ((execName || memberExec) && node.callee.type === 'MemberExpression' &&
1125
+ node.callee.object?.type === 'CallExpression') {
1126
+ const innerCall = node.callee.object;
1127
+ const innerName = getCallName(innerCall);
1128
+ if (innerName === 'require' && innerCall.arguments.length > 0 &&
1129
+ innerCall.arguments[0]?.type === 'Literal' && innerCall.arguments[0].value === 'child_process') {
1130
+ const cmdArg = node.arguments[0];
1131
+ if (cmdArg && cmdArg.type !== 'Literal') {
1132
+ // Non-literal command argument with inline require — opaque shell execution
1133
+ ctx.hasDynamicExec = true;
1134
+ ctx.threats.push({
1135
+ type: 'dangerous_exec',
1136
+ severity: 'HIGH',
1137
+ message: `Inline require('child_process').${execName || memberExec}(variable) — hidden import with runtime-determined command. Typical C2 or RCE payload pattern.`,
1138
+ file: ctx.relFile
1139
+ });
1140
+ }
1141
+ }
1142
+ }
1143
+
1065
1144
  // Detect sandbox/container evasion
1066
1145
  if (node.callee.type === 'MemberExpression' && node.callee.property?.type === 'Identifier') {
1067
1146
  const fsMethod = node.callee.property.name;
@@ -2260,6 +2339,144 @@ function handleCallExpression(node, ctx) {
2260
2339
  }
2261
2340
  }
2262
2341
 
2342
+ // Blue Team v8: process.dlopen() — direct native module loading bypass
2343
+ if (node.callee.type === 'MemberExpression' &&
2344
+ node.callee.object?.type === 'Identifier' && node.callee.object.name === 'process' &&
2345
+ node.callee.property?.type === 'Identifier' && node.callee.property.name === 'dlopen' &&
2346
+ node.arguments.length >= 1) {
2347
+ ctx.threats.push({
2348
+ type: 'process_binding_abuse',
2349
+ severity: 'CRITICAL',
2350
+ message: 'process.dlopen() — direct native module loading bypasses require() and all module system checks.',
2351
+ file: ctx.relFile
2352
+ });
2353
+ }
2354
+
2355
+ // Blue Team v8: dgram.createSocket() / socket.send() — UDP exfiltration tracking
2356
+ if (node.callee.type === 'MemberExpression' && node.callee.property?.type === 'Identifier') {
2357
+ const dgramMethod = node.callee.property.name;
2358
+ if (dgramMethod === 'createSocket' && ctx.hasDgramImport) {
2359
+ ctx.hasDgramSend = true; // createSocket implies intent to use UDP
2360
+ }
2361
+ if (dgramMethod === 'send' && ctx.hasDgramImport) {
2362
+ ctx.hasDgramSend = true;
2363
+ }
2364
+ }
2365
+
2366
+ // Blue Team v8: Crontab/cron write detection — fs.writeFileSync to cron paths
2367
+ if (node.callee.type === 'MemberExpression' && node.callee.property?.type === 'Identifier') {
2368
+ const cronWriteMethod = node.callee.property.name;
2369
+ if (['writeFileSync', 'writeFile', 'appendFileSync'].includes(cronWriteMethod) && node.arguments.length >= 1) {
2370
+ const cronPathArg = node.arguments[0];
2371
+ let cronPathStr = extractStringValueDeep(cronPathArg);
2372
+ if (!cronPathStr && cronPathArg?.type === 'Identifier' && ctx.stringVarValues?.has(cronPathArg.name)) {
2373
+ cronPathStr = ctx.stringVarValues.get(cronPathArg.name);
2374
+ }
2375
+ if (cronPathStr && (/\/etc\/cron/i.test(cronPathStr) || /crontab/i.test(cronPathStr) ||
2376
+ /\/var\/spool\/cron/i.test(cronPathStr))) {
2377
+ ctx.hasCrontabWrite = true;
2378
+ ctx.threats.push({
2379
+ type: 'crontab_systemd_write',
2380
+ severity: 'CRITICAL',
2381
+ message: `${cronWriteMethod}() writes to cron path: "${cronPathStr.substring(0, 80)}" — scheduled task persistence.`,
2382
+ file: ctx.relFile
2383
+ });
2384
+ }
2385
+ }
2386
+ }
2387
+
2388
+ // Blue Team v8: .replace() chain detection — 3+ chained .replace() calls for string mutation obfuscation
2389
+ // Pattern: 'l33t'.replace(/3/g, 'e').replace(/1/g, 'i') to reconstruct dangerous strings
2390
+ if (node.callee.type === 'MemberExpression' && node.callee.property?.type === 'Identifier' &&
2391
+ node.callee.property.name === 'replace' && node.arguments.length >= 2) {
2392
+ // Walk up the chain to count .replace() depth and try to resolve
2393
+ let depth = 1;
2394
+ let baseNode = node.callee.object;
2395
+ while (baseNode?.type === 'CallExpression' &&
2396
+ baseNode.callee?.type === 'MemberExpression' &&
2397
+ baseNode.callee.property?.type === 'Identifier' &&
2398
+ baseNode.callee.property.name === 'replace' &&
2399
+ baseNode.arguments?.length >= 2) {
2400
+ depth++;
2401
+ baseNode = baseNode.callee.object;
2402
+ }
2403
+ if (depth >= 3) {
2404
+ // Try to statically resolve the chain
2405
+ let resolved = null;
2406
+ if (baseNode?.type === 'Literal' && typeof baseNode.value === 'string') {
2407
+ resolved = baseNode.value;
2408
+ } else if (baseNode?.type === 'Identifier' && ctx.stringVarValues?.has(baseNode.name)) {
2409
+ resolved = ctx.stringVarValues.get(baseNode.name);
2410
+ }
2411
+ if (resolved !== null) {
2412
+ // Apply each .replace() in order (walk the chain from inner to outer)
2413
+ const replaceCalls = [];
2414
+ let currentNode = node;
2415
+ while (currentNode?.type === 'CallExpression' &&
2416
+ currentNode.callee?.type === 'MemberExpression' &&
2417
+ currentNode.callee.property?.name === 'replace') {
2418
+ replaceCalls.unshift(currentNode.arguments);
2419
+ currentNode = currentNode.callee.object;
2420
+ }
2421
+ for (const args of replaceCalls) {
2422
+ if (args.length >= 2) {
2423
+ let pattern = null;
2424
+ let replacement = null;
2425
+ // Extract regex or string pattern
2426
+ if (args[0].type === 'Literal' && args[0].regex) {
2427
+ try { pattern = new RegExp(args[0].regex.pattern, args[0].regex.flags); } catch { /* skip */ }
2428
+ } else if (args[0].type === 'Literal' && typeof args[0].value === 'string') {
2429
+ pattern = args[0].value;
2430
+ }
2431
+ if (args[1].type === 'Literal' && typeof args[1].value === 'string') {
2432
+ replacement = args[1].value;
2433
+ }
2434
+ if (pattern !== null && replacement !== null) {
2435
+ resolved = resolved.replace(pattern, replacement);
2436
+ } else {
2437
+ resolved = null;
2438
+ break;
2439
+ }
2440
+ }
2441
+ }
2442
+ }
2443
+ const DANGEROUS_REPLACE_KEYWORDS = /\b(child_process|eval|exec|spawn|Function|http|net|dns|require|process)\b/;
2444
+ if (resolved && DANGEROUS_REPLACE_KEYWORDS.test(resolved)) {
2445
+ ctx.threats.push({
2446
+ type: 'string_mutation_obfuscation',
2447
+ severity: 'HIGH',
2448
+ message: `String mutation via ${depth} chained .replace() calls resolves to "${resolved.substring(0, 80)}" — leet-speak/substitution evasion.`,
2449
+ file: ctx.relFile
2450
+ });
2451
+ } else if (depth >= 4) {
2452
+ // 4+ replace chains without resolution is still suspicious
2453
+ ctx.threats.push({
2454
+ type: 'string_mutation_obfuscation',
2455
+ severity: 'MEDIUM',
2456
+ message: `${depth} chained .replace() calls detected — potential string mutation obfuscation (could not fully resolve).`,
2457
+ file: ctx.relFile
2458
+ });
2459
+ }
2460
+ }
2461
+ }
2462
+
2463
+ // Blue Team v8b (A1): X.apply(require, null, [...]) — indirect require via Reflect.apply
2464
+ // Detect Reflect.apply(require, ...) or anyVar.apply(require, ...) pattern
2465
+ // This is an evasion where the attacker uses reflection to invoke require dynamically
2466
+ if (node.callee?.type === 'MemberExpression' &&
2467
+ node.callee.property?.type === 'Identifier' && node.callee.property.name === 'apply' &&
2468
+ node.arguments.length >= 2) {
2469
+ const firstArg = node.arguments[0];
2470
+ if (firstArg?.type === 'Identifier' && firstArg.name === 'require') {
2471
+ ctx.threats.push({
2472
+ type: 'dynamic_require',
2473
+ severity: 'CRITICAL',
2474
+ message: '.apply(require, ...) — indirect require() invocation via Reflect.apply or Function.prototype.apply. Evasion technique to dynamically load modules.',
2475
+ file: ctx.relFile
2476
+ });
2477
+ }
2478
+ }
2479
+
2263
2480
  // Audit v3 bypass fix: process.on('uncaughtException'/'unhandledRejection', handler)
2264
2481
  // Error handler hijacking for silent credential exfiltration
2265
2482
  if (node.callee?.type === 'MemberExpression' &&
@@ -2274,6 +2491,116 @@ function handleCallExpression(node, ctx) {
2274
2491
  }
2275
2492
 
2276
2493
  // SANDWORM_MODE R8: dns.resolve detection moved to walk.ancestor() in ast.js (FIX 5)
2494
+
2495
+ // Blue Team v8b (A7): JSON.parse with reviver that checks __proto__
2496
+ // JSON.parse(str, function(key, value) { if (key === '__proto__') ... })
2497
+ // Note: getCallName returns 'parse' (property only), so check object.name === 'JSON'
2498
+ if (callName === 'parse' && node.callee?.type === 'MemberExpression' &&
2499
+ node.callee.object?.type === 'Identifier' && node.callee.object.name === 'JSON' &&
2500
+ node.arguments.length >= 2) {
2501
+ const reviver = node.arguments[1];
2502
+ if (reviver && (reviver.type === 'FunctionExpression' || reviver.type === 'ArrowFunctionExpression')) {
2503
+ // Check if reviver body contains __proto__ reference
2504
+ const reviverSrc = reviver.start !== undefined && reviver.end !== undefined
2505
+ ? ctx._sourceCode?.slice(reviver.start, reviver.end) : '';
2506
+ if (reviverSrc && /__proto__|prototype\s*[.[]/.test(reviverSrc)) {
2507
+ ctx.hasJsonReviverProto = true;
2508
+ const hasRequireInReviver = /\brequire\s*\(/.test(reviverSrc);
2509
+ const hasProtoAssign = /Object\.prototype\s*\./.test(reviverSrc) || /\.__proto__\s*=/.test(reviverSrc);
2510
+ ctx.threats.push({
2511
+ type: 'json_reviver_pollution',
2512
+ severity: (hasRequireInReviver || hasProtoAssign) ? 'CRITICAL' : 'HIGH',
2513
+ message: (hasRequireInReviver || hasProtoAssign)
2514
+ ? 'JSON.parse reviver accesses __proto__/prototype with require() or prototype assignment — prototype pollution for code injection.'
2515
+ : 'JSON.parse reviver accesses __proto__/prototype — potential prototype pollution via untrusted JSON input.',
2516
+ file: ctx.relFile
2517
+ });
2518
+ }
2519
+ }
2520
+ }
2521
+
2522
+ // Blue Team v8b (C2): vm.runInContext/runInNewContext/compileFunction with dynamic code
2523
+ // Detect when the code argument is built from Buffer.from/base64/concat, not a string literal
2524
+ if (node.callee?.type === 'MemberExpression' && node.callee.property?.type === 'Identifier') {
2525
+ const vmMethods = ['runInContext', 'runInNewContext', 'runInThisContext', 'compileFunction'];
2526
+ if (vmMethods.includes(node.callee.property.name) && node.arguments.length > 0) {
2527
+ const codeArg = node.arguments[0];
2528
+ // Dynamic code: not a string literal, could be variable, concat, Buffer.from, etc.
2529
+ if (codeArg && codeArg.type !== 'Literal') {
2530
+ const isDynamic = codeArg.type === 'Identifier' || codeArg.type === 'BinaryExpression' ||
2531
+ codeArg.type === 'CallExpression' || codeArg.type === 'TemplateLiteral';
2532
+ if (isDynamic) {
2533
+ ctx.hasVmDynamicExec = true;
2534
+ ctx.hasDynamicExec = true;
2535
+ ctx.threats.push({
2536
+ type: 'vm_dynamic_code',
2537
+ severity: 'CRITICAL',
2538
+ message: `vm.${node.callee.property.name}() with dynamically constructed code — vm sandbox escape via runtime-built code string.`,
2539
+ file: ctx.relFile
2540
+ });
2541
+ }
2542
+ }
2543
+ }
2544
+ }
2545
+
2546
+ // Blue Team v8b (C2): vm.createContext() with custom require injection + sensitive modules
2547
+ // Detects sandbox setup that provides module access to untrusted code
2548
+ if (callName === 'createContext' && node.callee?.type === 'MemberExpression' &&
2549
+ node.callee.object?.type === 'Identifier') {
2550
+ const src = ctx._sourceCode || '';
2551
+ // Check if the same file defines a custom require for the sandbox
2552
+ const hasRequireInjection = /\.require\s*=\s*(?:\(|function\b)/.test(src) ||
2553
+ /require\s*:\s*(?:\(|function\b)/.test(src);
2554
+ const hasSensitiveModules = /require\s*\(\s*['"](?:fs|http|https|net|child_process)['"]/.test(src);
2555
+ if (hasRequireInjection && hasSensitiveModules) {
2556
+ ctx.hasDynamicExec = true;
2557
+ ctx.threats.push({
2558
+ type: 'vm_dynamic_code',
2559
+ severity: 'CRITICAL',
2560
+ message: 'vm.createContext() with custom require() injection and access to sensitive modules (fs/http/net) — sandbox that enables untrusted code execution with elevated privileges.',
2561
+ file: ctx.relFile
2562
+ });
2563
+ }
2564
+ }
2565
+
2566
+ // Blue Team v8b (B7): fs.readFileSync on image/binary files (stego pattern)
2567
+ if (node.callee?.type === 'MemberExpression' && node.callee.property?.type === 'Identifier' &&
2568
+ ['readFileSync', 'readFile'].includes(node.callee.property.name)) {
2569
+ const fileArg = node.arguments?.[0];
2570
+ let filePath = '';
2571
+ if (fileArg?.type === 'Literal' && typeof fileArg.value === 'string') {
2572
+ filePath = fileArg.value;
2573
+ } else if (fileArg?.type === 'Identifier' && ctx.stringVarValues?.has(fileArg.name)) {
2574
+ filePath = ctx.stringVarValues.get(fileArg.name);
2575
+ }
2576
+ if (/\.(png|jpg|jpeg|gif|bmp|ico|svg)$/i.test(filePath)) {
2577
+ ctx.hasBinaryFileRead = true;
2578
+ }
2579
+ }
2580
+
2581
+ // Blue Team v8b (C10): execSync/exec inside .on('message') or .on('data') callback
2582
+ if (node.callee?.type === 'MemberExpression' && node.callee.property?.type === 'Identifier' &&
2583
+ node.callee.property.name === 'on' && node.arguments.length >= 2) {
2584
+ const eventArg = node.arguments[0];
2585
+ if (eventArg?.type === 'Literal' && ['message', 'data'].includes(eventArg.value)) {
2586
+ const callback = node.arguments[1];
2587
+ if (callback && (callback.type === 'FunctionExpression' || callback.type === 'ArrowFunctionExpression')) {
2588
+ const cbSrc = callback.start !== undefined && callback.end !== undefined
2589
+ ? ctx._sourceCode?.slice(callback.start, callback.end) : '';
2590
+ if (cbSrc && /\b(execSync|exec|spawn|spawnSync)\s*\(/.test(cbSrc) &&
2591
+ /\brequire\s*\(\s*['"]child_process['"]\s*\)/.test(cbSrc)) {
2592
+ ctx.hasCallbackExec = true;
2593
+ ctx.hasDynamicExec = true;
2594
+ ctx.threats.push({
2595
+ type: 'callback_exec_rce',
2596
+ severity: 'CRITICAL',
2597
+ message: `exec/spawn inside .on('${eventArg.value}') callback with require('child_process') — remote command execution from network input.`,
2598
+ file: ctx.relFile
2599
+ });
2600
+ }
2601
+ }
2602
+ }
2603
+ }
2277
2604
  }
2278
2605
 
2279
2606
  function handleImportExpression(node, ctx) {
@@ -2298,10 +2625,16 @@ function handleImportExpression(node, ctx) {
2298
2625
  ctx.hasSolanaImport = true;
2299
2626
  }
2300
2627
  } else {
2628
+ // Blue Team v8b (C6): Dynamic import with non-literal arg — if it's a variable
2629
+ // built from URL manipulation, this is remote code loading
2630
+ const isCritical = node.source.type === 'Identifier' || node.source.type === 'TemplateLiteral' ||
2631
+ (node.source.type === 'CallExpression' && node.source.callee?.property?.name === 'replace');
2301
2632
  ctx.threats.push({
2302
2633
  type: 'dynamic_import',
2303
- severity: 'HIGH',
2304
- message: 'Dynamic import() with computed argument (possible obfuscation).',
2634
+ severity: isCritical ? 'CRITICAL' : 'HIGH',
2635
+ message: isCritical
2636
+ ? 'Dynamic import() with computed URL argument — remote code loading from dynamically constructed URL.'
2637
+ : 'Dynamic import() with computed argument (possible obfuscation).',
2305
2638
  file: ctx.relFile
2306
2639
  });
2307
2640
  }
@@ -2384,17 +2717,59 @@ function handleNewExpression(node, ctx) {
2384
2717
 
2385
2718
  // Batch 2: new Worker(code, { eval: true }) — worker_threads code execution
2386
2719
  if (node.callee.type === 'Identifier' && node.callee.name === 'Worker' &&
2387
- node.arguments.length >= 2) {
2388
- const opts = node.arguments[1];
2389
- if (opts?.type === 'ObjectExpression') {
2390
- const evalProp = opts.properties?.find(p =>
2391
- p.key?.name === 'eval' && p.value?.value === true);
2392
- if (evalProp) {
2393
- ctx.hasDynamicExec = true;
2720
+ node.arguments.length >= 1) {
2721
+ ctx.hasWorkerThread = true;
2722
+ if (node.arguments.length >= 2) {
2723
+ const opts = node.arguments[1];
2724
+ if (opts?.type === 'ObjectExpression') {
2725
+ const evalProp = opts.properties?.find(p =>
2726
+ p.key?.name === 'eval' && p.value?.value === true);
2727
+ if (evalProp) {
2728
+ ctx.hasDynamicExec = true;
2729
+ ctx.threats.push({
2730
+ type: 'worker_thread_exec',
2731
+ severity: 'HIGH',
2732
+ message: 'new Worker() with eval:true — executes arbitrary code in worker thread, bypasses main thread detection.',
2733
+ file: ctx.relFile
2734
+ });
2735
+ }
2736
+ }
2737
+ }
2738
+ // Blue Team v8: new Worker('data:...') — data URL code injection into worker
2739
+ const firstArg = node.arguments[0];
2740
+ if (firstArg?.type === 'Literal' && typeof firstArg.value === 'string' &&
2741
+ firstArg.value.startsWith('data:')) {
2742
+ ctx.hasDynamicExec = true;
2743
+ ctx.threats.push({
2744
+ type: 'worker_thread_exec',
2745
+ severity: 'HIGH',
2746
+ message: 'new Worker() with data: URL — inline code injection into worker thread.',
2747
+ file: ctx.relFile
2748
+ });
2749
+ }
2750
+ }
2751
+
2752
+ // Blue Team v8: new SharedArrayBuffer() — shared memory for covert IPC
2753
+ if (node.callee.type === 'Identifier' && node.callee.name === 'SharedArrayBuffer') {
2754
+ ctx.hasSharedArrayBuffer = true;
2755
+ }
2756
+
2757
+ // Blue Team v8: new WebSocket(url) — track for C2 compound detection
2758
+ if (node.callee.type === 'Identifier' && node.callee.name === 'WebSocket' &&
2759
+ node.arguments.length >= 1) {
2760
+ ctx.hasWebSocketNew = true;
2761
+ // Check if URL points to suspicious domain
2762
+ const wsArg = node.arguments[0];
2763
+ const wsUrl = extractStringValueDeep(wsArg);
2764
+ if (wsUrl) {
2765
+ const wsLower = wsUrl.toLowerCase();
2766
+ const isSuspiciousWs = SUSPICIOUS_DOMAINS_HIGH.some(d => wsLower.includes(d)) ||
2767
+ SUSPICIOUS_DOMAINS_MEDIUM.some(d => wsLower.includes(d));
2768
+ if (isSuspiciousWs) {
2394
2769
  ctx.threats.push({
2395
- type: 'worker_thread_exec',
2770
+ type: 'websocket_c2',
2396
2771
  severity: 'HIGH',
2397
- message: 'new Worker() with eval:true executes arbitrary code in worker thread, bypasses main thread detection.',
2772
+ message: `new WebSocket() connecting to suspicious domain: "${wsUrl.substring(0, 80)}" potential C2 channel.`,
2398
2773
  file: ctx.relFile
2399
2774
  });
2400
2775
  }
@@ -2609,6 +2984,32 @@ function handleAssignmentExpression(node, ctx) {
2609
2984
  }
2610
2985
  }
2611
2986
 
2987
+ // Blue Team v8b (A4): Module._resolveFilename / _compile / _extensions hijack
2988
+ // Any assignment to these private Module APIs is a supply-chain attack vector
2989
+ if (node.left?.type === 'MemberExpression' && !node.left.computed &&
2990
+ node.left.property?.type === 'Identifier' &&
2991
+ ['_resolveFilename', '_compile', '_extensions', '_findPath', '_nodeModulePaths'].includes(node.left.property.name)) {
2992
+ // Check if the object is Module, a module alias, or a constructor chain
2993
+ const obj = node.left.object;
2994
+ const isModuleRef = (obj?.type === 'Identifier' && (ctx.moduleAliases?.has(obj.name) || obj.name === 'Module')) ||
2995
+ // require('module')._resolveFilename = ...
2996
+ (obj?.type === 'CallExpression' && getCallName(obj) === 'require' && obj.arguments?.[0]?.value === 'module') ||
2997
+ // x.constructor._resolveFilename = ... (any .constructor chain)
2998
+ (obj?.type === 'MemberExpression' && obj.property?.type === 'Identifier' && obj.property.name === 'constructor');
2999
+ // Also match: proc.mainModule.constructor._resolveFilename (deeper chain)
3000
+ const isDeepChain = obj?.type === 'MemberExpression' && obj.property?.type === 'Identifier' &&
3001
+ ['_resolveFilename', '_compile', '_extensions', '_findPath', '_nodeModulePaths'].includes(node.left.property.name);
3002
+ if (isModuleRef || isDeepChain || (obj?.type === 'MemberExpression' && obj.property?.name === 'constructor')) {
3003
+ ctx.hasModuleInternalsHijack = true;
3004
+ ctx.threats.push({
3005
+ type: 'module_internals_hijack',
3006
+ severity: 'CRITICAL',
3007
+ message: `Assignment to Module.${node.left.property.name} — module system internals hijacked. All subsequent require() calls can be intercepted.`,
3008
+ file: ctx.relFile
3009
+ });
3010
+ }
3011
+ }
3012
+
2612
3013
  // Detect object property indirection: obj.exec = require('child_process').exec
2613
3014
  // or obj.fn = eval — stashing dangerous functions in object properties
2614
3015
  if (node.left?.type === 'MemberExpression' && node.right) {
@@ -3188,6 +3589,150 @@ function handlePostWalk(ctx) {
3188
3589
  file: ctx.relFile
3189
3590
  });
3190
3591
  }
3592
+
3593
+ // Blue Team v8: SharedArrayBuffer + Worker = covert shared memory IPC
3594
+ // Pattern: SharedArrayBuffer enables worker-to-main-thread communication
3595
+ // that bypasses message channel monitoring. Alone = MEDIUM, with exec = HIGH.
3596
+ if (ctx.hasSharedArrayBuffer && ctx.hasWorkerThread) {
3597
+ ctx.threats.push({
3598
+ type: 'shared_memory_ipc',
3599
+ severity: ctx.hasDynamicExec ? 'HIGH' : 'MEDIUM',
3600
+ message: ctx.hasDynamicExec
3601
+ ? 'SharedArrayBuffer + Worker + dynamic execution — covert shared memory IPC with code execution.'
3602
+ : 'SharedArrayBuffer + Worker — shared memory IPC channel bypasses standard message monitoring.',
3603
+ file: ctx.relFile
3604
+ });
3605
+ }
3606
+
3607
+ // Blue Team v8: dgram/UDP exfiltration — dgram import + send in same file
3608
+ if (ctx.hasDgramImport && ctx.hasDgramSend) {
3609
+ const hasExfilSignal = ctx.threats.some(t =>
3610
+ t.file === ctx.relFile && (t.type === 'env_access' || t.type === 'sensitive_string')
3611
+ );
3612
+ ctx.threats.push({
3613
+ type: 'udp_exfiltration',
3614
+ severity: hasExfilSignal ? 'CRITICAL' : 'HIGH',
3615
+ message: hasExfilSignal
3616
+ ? 'UDP socket (dgram) with credential/env access — data exfiltration via UDP bypasses HTTP-level monitoring.'
3617
+ : 'UDP socket (dgram) createSocket + send — non-HTTP data channel. UDP exfiltration bypasses HTTP firewalls.',
3618
+ file: ctx.relFile
3619
+ });
3620
+ }
3621
+
3622
+ // Blue Team v8: WebSocket C2 compound — WebSocket + exec/spawn in same file
3623
+ if (ctx.hasWebSocketNew && ctx.hasDynamicExec) {
3624
+ // Only emit if no websocket_c2 already emitted (from suspicious domain detection)
3625
+ if (!ctx.threats.some(t => t.type === 'websocket_c2' && t.file === ctx.relFile)) {
3626
+ ctx.threats.push({
3627
+ type: 'websocket_c2',
3628
+ severity: 'HIGH',
3629
+ message: 'WebSocket connection + dynamic execution in same file — potential WebSocket C2 channel for remote command execution.',
3630
+ file: ctx.relFile
3631
+ });
3632
+ }
3633
+ }
3634
+
3635
+ // Blue Team v8: Extended blockchain C2 — Ethereum/Web3 import + C2 methods
3636
+ // Extends GlassWorm detection to cover ethers.js/web3.js patterns
3637
+ if (!ctx.hasSolanaImport && ctx.hasSolanaC2Method) {
3638
+ // C2 method detected without Solana import — check if Ethereum packages are used
3639
+ const hasEthImport = /\brequire\s*\(\s*['"](?:ethers|web3|@ethersproject)/i.test(ctx._sourceCode || '');
3640
+ if (hasEthImport) {
3641
+ ctx.threats.push({
3642
+ type: 'blockchain_c2_resolution',
3643
+ severity: ctx.hasDynamicExec ? 'CRITICAL' : 'HIGH',
3644
+ message: 'Ethereum/Web3 import + blockchain C2 method — ' +
3645
+ (ctx.hasDynamicExec
3646
+ ? 'dead drop resolver with dynamic execution. Blockchain C2 pattern confirmed.'
3647
+ : 'potential dead drop resolver. Technique: C2 commands stored in blockchain transactions.'),
3648
+ file: ctx.relFile
3649
+ });
3650
+ }
3651
+ }
3652
+
3653
+ // Blue Team v8b (A6): with statement + Proxy + require/exec in same file = sandbox escape compound
3654
+ // Boost: if with_body_dangerous AND proxy trap detected → ensure minimum CRITICAL score
3655
+ const hasWithDangerous = ctx.threats.some(t => t.type === 'with_body_dangerous' && t.file === ctx.relFile);
3656
+ const hasProxyInFile = ctx.hasProxyTrap || ctx.proxyHandlerVars?.size > 0;
3657
+ if (hasWithDangerous && hasProxyInFile) {
3658
+ // Elevate existing with_body_dangerous to CRITICAL if not already
3659
+ for (const t of ctx.threats) {
3660
+ if (t.type === 'with_body_dangerous' && t.file === ctx.relFile && t.severity !== 'CRITICAL') {
3661
+ t.severity = 'CRITICAL';
3662
+ t.message = 'with() + Proxy compound: Proxy trap intercepts all scope resolution inside with block — complete API hijacking for sandbox escape.';
3663
+ }
3664
+ }
3665
+ }
3666
+
3667
+ // Blue Team v8b (B7): Binary file read + new Function/eval = steganographic payload
3668
+ if (ctx.hasBinaryFileRead && ctx.hasDynamicExec) {
3669
+ ctx.threats.push({
3670
+ type: 'stego_binary_exec',
3671
+ severity: 'CRITICAL',
3672
+ message: 'Binary/image file read + dynamic execution (eval/Function) — steganographic payload extraction and execution.',
3673
+ file: ctx.relFile
3674
+ });
3675
+ } else if (ctx.hasImageFileRef && ctx.hasDynamicExec && ctx.hasWriteFileSyncInContent) {
3676
+ // Fallback: image reference + eval in same file (may not directly readFile the image)
3677
+ ctx.threats.push({
3678
+ type: 'stego_binary_exec',
3679
+ severity: 'HIGH',
3680
+ message: 'Image file reference + dynamic execution + file I/O in same file — potential steganographic payload pattern.',
3681
+ file: ctx.relFile
3682
+ });
3683
+ }
3684
+
3685
+ // Blue Team v8b (C1): AsyncLocalStorage + credential file read + exec/network
3686
+ // Trigger on hasDynamicExec OR when dynamic_require is present (require('child_' + 'process') stored in context)
3687
+ const hasDynReqInFile = ctx.threats.some(t => t.type === 'dynamic_require' && t.file === ctx.relFile);
3688
+ if (ctx.hasAsyncLocalStorage && (ctx.hasDynamicExec || hasDynReqInFile)) {
3689
+ ctx.threats.push({
3690
+ type: 'asynclocal_context_exec',
3691
+ severity: 'HIGH',
3692
+ message: 'AsyncLocalStorage + dynamic execution/require — code execution hidden in async context, evades synchronous call-stack analysis.',
3693
+ file: ctx.relFile
3694
+ });
3695
+ }
3696
+
3697
+ // Blue Team v8b (C10): net.Socket + exec = WebSocket/TCP C2
3698
+ // Trigger on callbackExec (exec in .on('message') callback) OR hasDynamicExec (exec anywhere in file)
3699
+ if (ctx.hasNetSocketCreate && (ctx.hasCallbackExec || ctx.hasDynamicExec)) {
3700
+ if (!ctx.threats.some(t => t.type === 'websocket_c2' && t.file === ctx.relFile)) {
3701
+ ctx.threats.push({
3702
+ type: 'websocket_c2',
3703
+ severity: 'CRITICAL',
3704
+ message: 'net.Socket + exec in same file — persistent TCP/WebSocket C2 with remote command execution.',
3705
+ file: ctx.relFile
3706
+ });
3707
+ }
3708
+ }
3709
+
3710
+ // Blue Team v8b (B2): CI environment fingerprinting probe — 3+ CI provider env vars in same file
3711
+ // Indicates multi-provider CI detection for conditional payload activation
3712
+ if (ctx.ciProviderCount >= 3) {
3713
+ ctx.threats.push({
3714
+ type: 'ci_environment_probe',
3715
+ severity: 'HIGH',
3716
+ message: `File references ${ctx.ciProviderCount} CI provider environment variables (GITHUB_ACTIONS, GITLAB_CI, etc.) — CI environment fingerprinting for targeted execution.`,
3717
+ file: ctx.relFile
3718
+ });
3719
+ }
3720
+
3721
+ // Blue Team v8: Hardcoded contract address (40-char hex) + blockchain import = C2 address
3722
+ if ((ctx.hasSolanaImport || /\brequire\s*\(\s*['"](?:ethers|web3|@ethersproject|@solana)/i.test(ctx._sourceCode || '')) &&
3723
+ /\b0x[0-9a-fA-F]{40}\b/.test(ctx._sourceCode || '')) {
3724
+ const existingBlockchain = ctx.threats.some(t =>
3725
+ t.type === 'blockchain_c2_resolution' && t.file === ctx.relFile
3726
+ );
3727
+ if (!existingBlockchain) {
3728
+ ctx.threats.push({
3729
+ type: 'blockchain_c2_resolution',
3730
+ severity: 'MEDIUM',
3731
+ message: 'Blockchain import + hardcoded contract address (0x...) — potential smart contract C2 endpoint.',
3732
+ file: ctx.relFile
3733
+ });
3734
+ }
3735
+ }
3191
3736
  }
3192
3737
 
3193
3738
  function handleWithStatement(node, ctx) {
@@ -3225,10 +3770,14 @@ function handleWithStatement(node, ctx) {
3225
3770
  const bodySource = node.body.start !== undefined && node.body.end !== undefined
3226
3771
  ? ctx._sourceCode?.slice(node.body.start, node.body.end) : null;
3227
3772
  if (bodySource && /\b(require\s*\(\s*['"]child_process['"]\s*\)|child_process|exec\s*\(|execSync\s*\(|spawn\s*\()/.test(bodySource)) {
3773
+ // Blue Team v8: Elevate to CRITICAL when with() scope object is a known Proxy variable
3774
+ const isProxyScope = node.object?.type === 'Identifier' && ctx.proxyHandlerVars?.has(node.object.name);
3228
3775
  ctx.threats.push({
3229
3776
  type: 'with_body_dangerous',
3230
- severity: 'HIGH',
3231
- message: 'with() statement body contains require/exec/spawn — scope injection used to obscure dangerous API calls.',
3777
+ severity: isProxyScope ? 'CRITICAL' : 'HIGH',
3778
+ message: isProxyScope
3779
+ ? `with(Proxy) + exec/require in body — Proxy trap intercepts all name resolution, enabling complete API hijacking.`
3780
+ : 'with() statement body contains require/exec/spawn — scope injection used to obscure dangerous API calls.',
3232
3781
  file: ctx.relFile
3233
3782
  });
3234
3783
  }
@@ -75,6 +75,32 @@ function analyzeFile(content, filePath, basePath) {
75
75
  file: path.relative(basePath, filePath)
76
76
  });
77
77
  }
78
+
79
+ // Blue Team v8b (A6): Detect Proxy + require('child_process') + exec in files that fail to parse
80
+ // This covers 'use strict' + with(Proxy) evasion where acorn can't parse the with statement
81
+ if (/\bnew\s+Proxy\b/.test(content) && /\brequire\s*\(\s*['"]child_process['"]\s*\)/.test(content)) {
82
+ const hasExecInContent = /\bexec\s*\(/.test(content) || /\bexecSync\s*\(/.test(content) || /\bspawn\s*\(/.test(content);
83
+ if (hasExecInContent) {
84
+ threats.push({
85
+ type: 'dangerous_exec',
86
+ severity: 'CRITICAL',
87
+ message: 'Proxy + require(\'child_process\') + exec in unparseable file — scope hijack evasion (regex fallback).',
88
+ file: path.relative(basePath, filePath)
89
+ });
90
+ }
91
+ }
92
+
93
+ // Content-level: require('child_process') + exec/spawn with shell command patterns
94
+ if (/\brequire\s*\(\s*['"]child_process['"]\s*\)/.test(content) &&
95
+ /\bcurl\b.*\|\s*(sh|bash)\b/.test(content)) {
96
+ threats.push({
97
+ type: 'dangerous_exec',
98
+ severity: 'CRITICAL',
99
+ message: 'require(\'child_process\') + curl pipe to shell in unparseable file — remote code execution (regex fallback).',
100
+ file: path.relative(basePath, filePath)
101
+ });
102
+ }
103
+
78
104
  return threats;
79
105
  }
80
106
 
@@ -183,6 +209,37 @@ function analyzeFile(content, filePath, basePath) {
183
209
  hasUncaughtExceptionHandler: false,
184
210
  // Audit v3 B2: FinalizationRegistry deferred exec detection
185
211
  hasFinalizationRegistry: false,
212
+ // Blue Team v8: SharedArrayBuffer + Worker IPC detection
213
+ hasSharedArrayBuffer: false,
214
+ hasWorkerThread: false, // set when Worker (worker_threads) usage detected
215
+ // Blue Team v8: dgram/UDP exfiltration
216
+ hasDgramImport: /\brequire\s*\(\s*['"](?:node:)?dgram['"]\s*\)/.test(content),
217
+ hasDgramSend: false,
218
+ // Blue Team v8: WebSocket C2
219
+ hasWebSocketNew: false, // set when new WebSocket() detected
220
+ // Blue Team v8: crontab/cron write detection
221
+ hasCrontabWrite: false,
222
+ // Blue Team v8b: Module internals hijack (Module._resolveFilename, _compile, _extensions)
223
+ hasModuleInternalsHijack: false,
224
+ // Blue Team v8b: JSON.parse reviver with __proto__ check
225
+ hasJsonReviverProto: false,
226
+ // Blue Team v8b: vm.runInContext/runInNewContext with dynamic code
227
+ hasVmDynamicExec: false,
228
+ // Blue Team v8b: binary file read + new Function/eval in same file (stego)
229
+ hasBinaryFileRead: false, // set when fs.readFileSync on .png/.jpg/.gif/.bmp/.ico
230
+ // Blue Team v8b: AsyncLocalStorage usage
231
+ hasAsyncLocalStorage: /\bAsyncLocalStorage\b/.test(content),
232
+ // Blue Team v8b: image file reference for stego detection
233
+ hasImageFileRef: /\.(png|jpg|jpeg|gif|bmp|ico)\b/i.test(content),
234
+ // Blue Team v8b: net.Socket creation (for WebSocket C2 detection)
235
+ hasNetSocketCreate: /\bnew\s+net\.Socket\b/.test(content) || /\bnet\.createConnection\b/.test(content),
236
+ // Blue Team v8b: execSync/exec in callback contexts (set when exec inside .on('message'|'data'))
237
+ hasCallbackExec: false,
238
+ // Blue Team v8b (B2): CI environment fingerprinting — count of CI provider env vars referenced
239
+ ciProviderCount: (() => {
240
+ const CI_VARS = ['GITHUB_ACTIONS', 'GITLAB_CI', 'CIRCLECI', 'TRAVIS', 'JENKINS_URL', 'BUILDKITE', 'CONTINUOUS_INTEGRATION', 'TEAMCITY_VERSION', 'CODEBUILD_BUILD_ID', 'BITBUCKET_PIPELINE_UUID'];
241
+ return CI_VARS.filter(v => content.includes(v)).length;
242
+ })(),
186
243
  // Audit v3: source code reference for callback body analysis
187
244
  _sourceCode: content
188
245
  };
@@ -125,6 +125,25 @@ async function scanPackageJson(targetPath) {
125
125
  file: 'package.json'
126
126
  });
127
127
  }
128
+
129
+ // Blue Team v8b (B8): Lifecycle script references non-existent file in package
130
+ // Pattern: "node path/to/script.js" where the file does not exist — phantom install script
131
+ // Strong signal: preinstall/install scripts pointing to missing files can't be build artifacts
132
+ if (['preinstall', 'install', 'postinstall'].includes(scriptName)) {
133
+ const nodeFileMatch = scriptContent.match(/^node\s+(\S+)/);
134
+ if (nodeFileMatch) {
135
+ const scriptFile = nodeFileMatch[1];
136
+ const fullScriptPath = path.join(targetPath, scriptFile);
137
+ if (!fs.existsSync(fullScriptPath) && !fs.existsSync(fullScriptPath + '.js')) {
138
+ threats.push({
139
+ type: 'lifecycle_missing_script',
140
+ severity: scriptName === 'postinstall' ? 'HIGH' : 'CRITICAL',
141
+ message: `Lifecycle "${scriptName}" references "${scriptFile}" which does not exist in the package — phantom install script, payload may be injected at publish time.`,
142
+ file: 'package.json'
143
+ });
144
+ }
145
+ }
146
+ }
128
147
  }
129
148
  }
130
149
 
@@ -181,6 +200,57 @@ async function scanPackageJson(targetPath) {
181
200
  } catch { /* permission error */ }
182
201
  }
183
202
 
203
+ // Blue Team v8: binding.gyp + lifecycle script = native addon install risk
204
+ // binding.gyp triggers node-gyp compilation during install. Combined with lifecycle scripts
205
+ // that aren't standard node-gyp build tools, this indicates potentially malicious native code.
206
+ const bindingGypPath = path.join(targetPath, 'binding.gyp');
207
+ if (fs.existsSync(bindingGypPath)) {
208
+ const hasInstallLifecycle = ['preinstall', 'install', 'postinstall'].some(s => scripts[s]);
209
+ const installScript = scripts.install || scripts.postinstall || scripts.preinstall || '';
210
+ // node-gyp rebuild / prebuild-install / cmake-js are legitimate native addon builders
211
+ const isStandardBuild = /\b(node-gyp|prebuild|cmake-js|napi|prebuildify|neon)\b/i.test(installScript);
212
+
213
+ // Blue Team v8b (C7): Check binding.gyp content for shell commands in actions
214
+ let gypContent = '';
215
+ try { gypContent = fs.readFileSync(bindingGypPath, 'utf8'); } catch {}
216
+ const hasShellActions = /\baction\b.*\bsh\b/.test(gypContent) || /\bcurl\b/.test(gypContent) ||
217
+ /\bwget\b/.test(gypContent) || /\$\(whoami\)/.test(gypContent) || /\$\(uname/.test(gypContent);
218
+ // Check if binding.gyp references C/C++ source files
219
+ const hasNativeSources = /\.(c|cc|cpp|cxx|h|hpp)\b/.test(gypContent);
220
+
221
+ if (hasShellActions) {
222
+ threats.push({
223
+ type: 'native_addon_install',
224
+ severity: 'CRITICAL',
225
+ message: `binding.gyp contains shell commands in build actions (curl/sh/whoami) — build-time code execution and exfiltration.`,
226
+ file: 'binding.gyp'
227
+ });
228
+ } else if (hasInstallLifecycle && !isStandardBuild) {
229
+ threats.push({
230
+ type: 'native_addon_install',
231
+ severity: 'HIGH',
232
+ message: `binding.gyp present with non-standard lifecycle script: "${installScript.substring(0, 100)}" — potential malicious native compilation.`,
233
+ file: 'package.json'
234
+ });
235
+ } else if (hasInstallLifecycle && hasNativeSources) {
236
+ // Standard build but with native C/C++ sources — HIGH (native code is opaque)
237
+ threats.push({
238
+ type: 'native_addon_install',
239
+ severity: 'HIGH',
240
+ message: `binding.gyp with C/C++ source files + lifecycle script — native addon compilation. Native code is opaque to static analysis.`,
241
+ file: 'package.json'
242
+ });
243
+ } else if (hasInstallLifecycle) {
244
+ // Standard build tool — informational only
245
+ threats.push({
246
+ type: 'native_addon_install',
247
+ severity: 'LOW',
248
+ message: 'binding.gyp with standard build tool (node-gyp/prebuild) in lifecycle script — legitimate native addon.',
249
+ file: 'package.json'
250
+ });
251
+ }
252
+ }
253
+
184
254
  // Scan declared dependencies against IOCs
185
255
  let iocs;
186
256
  try {
package/src/scoring.js CHANGED
@@ -108,7 +108,11 @@ const PACKAGE_LEVEL_TYPES = new Set([
108
108
  'sandbox_canary_exfiltration',
109
109
  // Compound scoring rules — package-level co-occurrences
110
110
  'lifecycle_typosquat', 'lifecycle_inline_exec', 'lifecycle_remote_require',
111
- 'lifecycle_dataflow', 'lifecycle_dangerous_exec', 'obfuscated_lifecycle_env'
111
+ 'lifecycle_dataflow', 'lifecycle_dangerous_exec', 'obfuscated_lifecycle_env',
112
+ // Blue Team v8: package-level boost signals
113
+ 'isolated_suspicious_file', 'deep_suspicious_file',
114
+ // Blue Team v8b: phantom lifecycle scripts
115
+ 'lifecycle_missing_script'
112
116
  ]);
113
117
 
114
118
  /**
@@ -299,7 +303,8 @@ const REACHABILITY_EXEMPT_TYPES = new Set([
299
303
  'typosquat_detected', 'pypi_typosquat_detected',
300
304
  'pypi_malicious_package',
301
305
  'ai_config_injection', 'ai_config_injection_compound',
302
- 'detached_credential_exfil' // DPRK/Lazarus: invoked via lifecycle, not require/import
306
+ 'detached_credential_exfil', // DPRK/Lazarus: invoked via lifecycle, not require/import
307
+ 'native_addon_install' // binding.gyp executes during npm install but isn't require()'d
303
308
  ]);
304
309
 
305
310
  // ============================================
@@ -728,6 +733,50 @@ function applyFPReductions(threats, reachableFiles, packageName, packageDeps) {
728
733
  }
729
734
  }
730
735
 
736
+ /**
737
+ * Blue Team v8: Inject package-level boost threats that detect dissimulation patterns.
738
+ * Called within calculateRiskScore after file scores are computed.
739
+ * @param {Array} deduped - deduplicated threat array (mutated in place)
740
+ * @param {Object} fileScores - map of file → score
741
+ * @param {Map} fileGroups - map of file → threats array
742
+ * @param {Array} packageLevelThreats - package-level threats
743
+ */
744
+ function applyPackageLevelBoosts(deduped, fileScores, fileGroups, packageLevelThreats) {
745
+ const fileNames = Object.keys(fileScores);
746
+ const totalFiles = fileNames.length;
747
+
748
+ // 1. isolated_suspicious_file: exactly 1 file has score > 0, 10+ files have score 0
749
+ if (totalFiles >= 10) {
750
+ const filesWithScore = fileNames.filter(f => fileScores[f] > 0);
751
+ const filesWithZero = totalFiles - filesWithScore.length;
752
+ if (filesWithScore.length === 1 && filesWithZero >= 10) {
753
+ deduped.push({
754
+ type: 'isolated_suspicious_file',
755
+ severity: 'MEDIUM',
756
+ message: `Single suspicious file among ${totalFiles} files — potential dissimulation pattern (malicious code hidden in clean package).`,
757
+ file: filesWithScore[0],
758
+ boostSignal: true
759
+ });
760
+ }
761
+ }
762
+
763
+ // 2. deep_suspicious_file: finding in a file at depth > 3 from package root
764
+ for (const [file, threats] of fileGroups) {
765
+ if (!file || file === '(unknown)') continue;
766
+ const segments = file.replace(/\\/g, '/').split('/').filter(Boolean);
767
+ if (segments.length > 3 && threats.some(t => t.severity !== 'LOW')) {
768
+ deduped.push({
769
+ type: 'deep_suspicious_file',
770
+ severity: 'LOW',
771
+ message: `Suspicious pattern found in deeply nested file (depth ${segments.length}): ${file} — potential hiding technique.`,
772
+ file: file,
773
+ boostSignal: true
774
+ });
775
+ break; // Only emit once per package
776
+ }
777
+ }
778
+ }
779
+
731
780
  /**
732
781
  * Calculate per-file max risk score from deduplicated threats.
733
782
  * Formula: riskScore = min(100, max(file_scores + intent_bonus) + package_level_score)
@@ -795,6 +844,27 @@ function calculateRiskScore(deduped, intentResult) {
795
844
  crossFileBonus = Math.min(crossFileBonus, 25);
796
845
  }
797
846
 
847
+ // 5b. Blue Team v8: Package-level boost signals — detect dissimulation patterns
848
+ applyPackageLevelBoosts(deduped, fileScores, fileGroups, packageLevelThreats);
849
+ // Recompute packageScore after boosts may have added new package-level threats
850
+ const boostPackageThreats = deduped.filter(t => isPackageLevelThreat(t) && t.boostSignal);
851
+ if (boostPackageThreats.length > 0) {
852
+ packageScore = computeGroupScore([...packageLevelThreats, ...boostPackageThreats]);
853
+ if (packageScore >= 25 && [...packageLevelThreats, ...boostPackageThreats].some(t => t.severity === 'CRITICAL')) {
854
+ packageScore = Math.max(packageScore, 50);
855
+ }
856
+ }
857
+
858
+ // 5c. Blue Team v8: lifecycle_plus_finding boost — lifecycle + any finding = +10 package score
859
+ const hasActiveLifecycleForBoost = packageLevelThreats.some(t =>
860
+ t.type === 'lifecycle_script' && t.severity !== 'LOW'
861
+ );
862
+ const hasAnyFileFinding = fileLevelThreats.some(t => t.severity !== 'LOW');
863
+ let lifecycleBoost = 0;
864
+ if (hasActiveLifecycleForBoost && hasAnyFileFinding) {
865
+ lifecycleBoost = 10;
866
+ }
867
+
798
868
  // 6. Intent coherence bonus: additive score from source→sink pairs
799
869
  let intentBonus = 0;
800
870
  if (intentResult && intentResult.intentScore > 0) {
@@ -802,8 +872,8 @@ function calculateRiskScore(deduped, intentResult) {
802
872
  intentBonus = Math.min(intentResult.intentScore, 30);
803
873
  }
804
874
 
805
- // 7. Final score = max file score + cross-file bonus + intent bonus + package-level score, capped at 100
806
- const riskScore = Math.min(MAX_RISK_SCORE, maxFileScore + crossFileBonus + intentBonus + packageScore);
875
+ // 7. Final score = max file score + cross-file bonus + intent bonus + package-level score + lifecycle boost, capped at 100
876
+ const riskScore = Math.min(MAX_RISK_SCORE, maxFileScore + crossFileBonus + intentBonus + packageScore + lifecycleBoost);
807
877
 
808
878
  // 8. Old global score for comparison (sum of ALL findings)
809
879
  const globalRiskScore = computeGroupScore(deduped);
@@ -108,7 +108,7 @@ const crypto = require('crypto');
108
108
  * Key = sha256(code) + '|' + optionsKey (collision-free content-addressable key)
109
109
  */
110
110
  const _astCache = new Map();
111
- const _AST_CACHE_MAX = 600; // Max entries (one scan 500 files max)
111
+ const _AST_CACHE_MAX = 200; // Max entries (reduced from 600 to limit memory during evaluate)
112
112
 
113
113
  /**
114
114
  * Parse JS source with module-mode fallback to script-mode.