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 +22 -0
- package/package.json +1 -1
- package/src/response/playbooks.js +70 -0
- package/src/rules/index.js +206 -0
- package/src/scanner/ast-detectors.js +563 -14
- package/src/scanner/ast.js +57 -0
- package/src/scanner/package.js +70 -0
- package/src/scoring.js +74 -4
- package/src/shared/constants.js +1 -1
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
|
@@ -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) {
|
package/src/rules/index.js
CHANGED
|
@@ -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:
|
|
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 >=
|
|
2388
|
-
|
|
2389
|
-
if (
|
|
2390
|
-
const
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
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: '
|
|
2770
|
+
type: 'websocket_c2',
|
|
2396
2771
|
severity: 'HIGH',
|
|
2397
|
-
message:
|
|
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:
|
|
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
|
}
|
package/src/scanner/ast.js
CHANGED
|
@@ -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
|
};
|
package/src/scanner/package.js
CHANGED
|
@@ -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);
|
package/src/shared/constants.js
CHANGED
|
@@ -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 =
|
|
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.
|