muaddib-scanner 2.2.13 → 2.2.15
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/README.md +3 -9
- package/datasets/adversarial/indirect-eval-bypass/index.js +27 -0
- package/datasets/adversarial/indirect-eval-bypass/package.json +5 -0
- package/datasets/adversarial/mjs-extension-bypass/package.json +6 -0
- package/datasets/adversarial/mjs-extension-bypass/stealer.mjs +39 -0
- package/datasets/adversarial/muaddib-ignore-bypass/index.js +47 -0
- package/datasets/adversarial/muaddib-ignore-bypass/package.json +5 -0
- package/package.json +2 -2
- package/src/commands/evaluate.js +5 -1
- package/src/index.js +33 -589
- package/src/ioc/bootstrap.js +5 -4
- package/src/output-formatter.js +192 -0
- package/src/scanner/ast-detectors.js +933 -0
- package/src/scanner/ast.js +43 -936
- package/src/scanner/dataflow.js +7 -59
- package/src/scanner/deobfuscate.js +4 -18
- package/src/scanner/entropy.js +6 -24
- package/src/scanner/github-actions.js +2 -1
- package/src/scanner/hash.js +1 -1
- package/src/scanner/module-graph.js +3 -3
- package/src/scanner/npm-registry.js +4 -3
- package/src/scanner/obfuscation.js +4 -19
- package/src/scanner/shell.js +3 -13
- package/src/scanner/typosquat.js +6 -0
- package/src/scoring.js +213 -0
- package/src/shared/analyze-helper.js +49 -0
- package/src/shared/constants.js +5 -1
- package/src/temporal-ast-diff.js +8 -18
- package/src/temporal-runner.js +139 -0
- package/src/utils.js +89 -4
|
@@ -0,0 +1,933 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const { getCallName } = require('../utils.js');
|
|
3
|
+
|
|
4
|
+
// ============================================
|
|
5
|
+
// AST DETECTION CONSTANTS
|
|
6
|
+
// ============================================
|
|
7
|
+
|
|
8
|
+
const DANGEROUS_CALLS = [
|
|
9
|
+
'eval',
|
|
10
|
+
'Function'
|
|
11
|
+
];
|
|
12
|
+
|
|
13
|
+
const SENSITIVE_STRINGS = [
|
|
14
|
+
'.npmrc',
|
|
15
|
+
'.ssh',
|
|
16
|
+
'Shai-Hulud',
|
|
17
|
+
'The Second Coming',
|
|
18
|
+
'Goldox-T3chs',
|
|
19
|
+
'/etc/passwd',
|
|
20
|
+
'/etc/shadow'
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
// Env vars that are safe and should NOT be flagged (common config/runtime vars)
|
|
24
|
+
const SAFE_ENV_VARS = [
|
|
25
|
+
'NODE_ENV', 'PORT', 'HOST', 'HOSTNAME', 'PWD', 'HOME', 'PATH',
|
|
26
|
+
'LANG', 'TERM', 'CI', 'DEBUG', 'VERBOSE', 'LOG_LEVEL',
|
|
27
|
+
'SHELL', 'USER', 'LOGNAME', 'EDITOR', 'TZ',
|
|
28
|
+
'NODE_DEBUG', 'NODE_PATH', 'NODE_OPTIONS',
|
|
29
|
+
'DISPLAY', 'COLORTERM', 'FORCE_COLOR', 'NO_COLOR', 'TERM_PROGRAM'
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
// Env var prefixes that are safe (npm metadata, locale settings)
|
|
33
|
+
const SAFE_ENV_PREFIXES = ['npm_config_', 'npm_lifecycle_', 'npm_package_', 'lc_'];
|
|
34
|
+
|
|
35
|
+
// Env var keywords to detect sensitive environment access (separate from SENSITIVE_STRINGS)
|
|
36
|
+
const ENV_SENSITIVE_KEYWORDS = [
|
|
37
|
+
'TOKEN', 'SECRET', 'KEY', 'PASSWORD', 'CREDENTIAL', 'AUTH'
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
// AI agent dangerous flags — disable security controls (s1ngularity/Nx, Aug 2025)
|
|
41
|
+
const AI_AGENT_DANGEROUS_FLAGS = [
|
|
42
|
+
'--dangerously-skip-permissions',
|
|
43
|
+
'--yolo',
|
|
44
|
+
'--trust-all-tools',
|
|
45
|
+
'--yes-always',
|
|
46
|
+
'--no-permission-check'
|
|
47
|
+
];
|
|
48
|
+
|
|
49
|
+
// AI agent binary names
|
|
50
|
+
const AI_AGENT_BINARIES = ['claude', 'gemini', 'q', 'aider', 'copilot', 'cursor'];
|
|
51
|
+
|
|
52
|
+
// Strings that are NOT suspicious
|
|
53
|
+
const SAFE_STRINGS = [
|
|
54
|
+
'api.github.com',
|
|
55
|
+
'registry.npmjs.org',
|
|
56
|
+
'npmjs.com'
|
|
57
|
+
];
|
|
58
|
+
|
|
59
|
+
// Credential-stealing CLI commands (s1ngularity/Nx, Shai-Hulud)
|
|
60
|
+
const CREDENTIAL_CLI_COMMANDS = [
|
|
61
|
+
'gh auth token',
|
|
62
|
+
'gcloud auth print-access-token',
|
|
63
|
+
'aws sts get-session-token',
|
|
64
|
+
'az account get-access-token',
|
|
65
|
+
'heroku auth:token',
|
|
66
|
+
'netlify api --data',
|
|
67
|
+
'vercel whoami'
|
|
68
|
+
];
|
|
69
|
+
|
|
70
|
+
// Dangerous shell command patterns for variable tracking
|
|
71
|
+
const DANGEROUS_CMD_PATTERNS = [/\bcurl\b/, /\bwget\b/, /\bnc\s+-/, /\/dev\/tcp\//, /\bbash\s+-i/];
|
|
72
|
+
|
|
73
|
+
// Native APIs targeted for prototype hooking (chalk Sept 2025, Sygnia)
|
|
74
|
+
const HOOKABLE_NATIVES = [
|
|
75
|
+
'fetch', 'XMLHttpRequest', 'Request', 'Response',
|
|
76
|
+
'WebSocket', 'EventSource'
|
|
77
|
+
];
|
|
78
|
+
|
|
79
|
+
// Node.js core module classes targeted for prototype hooking
|
|
80
|
+
const NODE_HOOKABLE_MODULES = ['http', 'https', 'net', 'tls', 'stream'];
|
|
81
|
+
const NODE_HOOKABLE_CLASSES = [
|
|
82
|
+
'IncomingMessage', 'ServerResponse', 'ClientRequest',
|
|
83
|
+
'OutgoingMessage', 'Socket', 'Server', 'Agent'
|
|
84
|
+
];
|
|
85
|
+
|
|
86
|
+
// Paths indicating sandbox/container environment detection (anti-analysis)
|
|
87
|
+
const SANDBOX_INDICATORS = [
|
|
88
|
+
'/.dockerenv',
|
|
89
|
+
'/proc/1/cgroup',
|
|
90
|
+
'/proc/self/cgroup'
|
|
91
|
+
];
|
|
92
|
+
|
|
93
|
+
// ============================================
|
|
94
|
+
// HELPER FUNCTIONS
|
|
95
|
+
// ============================================
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Extract string value from a node if it's a Literal or TemplateLiteral with no expressions.
|
|
99
|
+
*/
|
|
100
|
+
function extractStringValue(node) {
|
|
101
|
+
if (!node) return null;
|
|
102
|
+
if (node.type === 'Literal' && typeof node.value === 'string') return node.value;
|
|
103
|
+
if (node.type === 'TemplateLiteral' && node.expressions.length === 0) {
|
|
104
|
+
return node.quasis.map(q => q.value.raw).join('');
|
|
105
|
+
}
|
|
106
|
+
// Template literal with expressions — concatenate what we can
|
|
107
|
+
if (node.type === 'TemplateLiteral') {
|
|
108
|
+
return node.quasis.map(q => q.value.raw).join('***');
|
|
109
|
+
}
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Returns true if all arguments of a call/new expression are string literals.
|
|
115
|
+
* Used to distinguish safe patterns like eval('1+2') or Function('return this')
|
|
116
|
+
* from dangerous dynamic patterns like eval(userInput).
|
|
117
|
+
*/
|
|
118
|
+
function hasOnlyStringLiteralArgs(node) {
|
|
119
|
+
if (!node.arguments || node.arguments.length === 0) return false;
|
|
120
|
+
return node.arguments.every(arg => arg.type === 'Literal' && typeof arg.value === 'string');
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Returns true if a node is a decode call: atob(str) or Buffer.from(str,'base64').toString()
|
|
125
|
+
* Used to detect staged eval/Function decode patterns.
|
|
126
|
+
*/
|
|
127
|
+
function hasDecodeArg(node) {
|
|
128
|
+
if (!node || typeof node !== 'object') return false;
|
|
129
|
+
// atob('...')
|
|
130
|
+
if (node.type === 'CallExpression' &&
|
|
131
|
+
node.callee?.type === 'Identifier' && node.callee.name === 'atob') {
|
|
132
|
+
return true;
|
|
133
|
+
}
|
|
134
|
+
// Buffer.from('...', 'base64').toString()
|
|
135
|
+
if (node.type === 'CallExpression' &&
|
|
136
|
+
node.callee?.type === 'MemberExpression' &&
|
|
137
|
+
node.callee.property?.name === 'toString') {
|
|
138
|
+
const inner = node.callee.object;
|
|
139
|
+
if (inner?.type === 'CallExpression' &&
|
|
140
|
+
inner.callee?.type === 'MemberExpression' &&
|
|
141
|
+
inner.callee.object?.name === 'Buffer' &&
|
|
142
|
+
inner.callee.property?.name === 'from' &&
|
|
143
|
+
inner.arguments?.length >= 2 &&
|
|
144
|
+
inner.arguments[1]?.value === 'base64') {
|
|
145
|
+
return true;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return false;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Checks if an AST subtree contains decode patterns (base64, atob, fromCharCode).
|
|
153
|
+
*/
|
|
154
|
+
function containsDecodePattern(node) {
|
|
155
|
+
if (!node || typeof node !== 'object') return false;
|
|
156
|
+
if (node.type === 'Literal' && node.value === 'base64') return true;
|
|
157
|
+
if (node.type === 'Identifier' && (node.name === 'atob' || node.name === 'fromCharCode')) return true;
|
|
158
|
+
for (const key of Object.keys(node)) {
|
|
159
|
+
if (key === 'type' || key === 'start' || key === 'end' || key === 'loc') continue;
|
|
160
|
+
const child = node[key];
|
|
161
|
+
if (Array.isArray(child)) {
|
|
162
|
+
for (const item of child) {
|
|
163
|
+
if (item && typeof item.type === 'string' && containsDecodePattern(item)) return true;
|
|
164
|
+
}
|
|
165
|
+
} else if (child && typeof child === 'object' && typeof child.type === 'string') {
|
|
166
|
+
if (containsDecodePattern(child)) return true;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return false;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// ============================================
|
|
173
|
+
// VISITOR HANDLERS
|
|
174
|
+
// ============================================
|
|
175
|
+
// Each handler receives (node, ctx) where ctx contains:
|
|
176
|
+
// threats, relFile, dynamicRequireVars, dangerousCmdVars,
|
|
177
|
+
// workflowPathVars, execPathVars, globalThisAliases,
|
|
178
|
+
// hasFromCharCode, hasEvalInFile (mutable)
|
|
179
|
+
|
|
180
|
+
function handleVariableDeclarator(node, ctx) {
|
|
181
|
+
if (node.id?.type === 'Identifier') {
|
|
182
|
+
// Track dynamic require vars
|
|
183
|
+
if (node.init?.type === 'CallExpression') {
|
|
184
|
+
const initCallName = getCallName(node.init);
|
|
185
|
+
if (initCallName === 'require' && node.init.arguments.length > 0) {
|
|
186
|
+
const arg = node.init.arguments[0];
|
|
187
|
+
if (arg.type !== 'Literal') {
|
|
188
|
+
ctx.dynamicRequireVars.add(node.id.name);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
// Track variables assigned dangerous command strings
|
|
193
|
+
const strVal = extractStringValue(node.init);
|
|
194
|
+
if (strVal && DANGEROUS_CMD_PATTERNS.some(p => p.test(strVal))) {
|
|
195
|
+
ctx.dangerousCmdVars.set(node.id.name, strVal);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Track variables assigned temp/executable file paths
|
|
199
|
+
if (strVal && /^\/tmp\/|^\/var\/tmp\/|\\temp\\/i.test(strVal)) {
|
|
200
|
+
ctx.execPathVars.set(node.id.name, strVal);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Track variables that alias globalThis or global (e.g. const g = globalThis)
|
|
204
|
+
if (node.init?.type === 'Identifier' &&
|
|
205
|
+
(node.init.name === 'globalThis' || node.init.name === 'global' ||
|
|
206
|
+
node.init.name === 'window' || node.init.name === 'self')) {
|
|
207
|
+
ctx.globalThisAliases.add(node.id.name);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Track variables assigned from path.join containing .github/workflows
|
|
211
|
+
if (node.init?.type === 'CallExpression' && node.init.callee?.type === 'MemberExpression') {
|
|
212
|
+
const obj = node.init.callee.object;
|
|
213
|
+
const prop = node.init.callee.property;
|
|
214
|
+
if (obj?.type === 'Identifier' && obj.name === 'path' &&
|
|
215
|
+
prop?.type === 'Identifier' && (prop.name === 'join' || prop.name === 'resolve')) {
|
|
216
|
+
const joinArgs = node.init.arguments.map(a => extractStringValue(a) || '').join('/');
|
|
217
|
+
if (/\.github[\\/\/]workflows/i.test(joinArgs) || /\.github[\\/\/]actions/i.test(joinArgs)) {
|
|
218
|
+
ctx.workflowPathVars.add(node.id.name);
|
|
219
|
+
}
|
|
220
|
+
// Propagate: path.join(workflowPathVar, ...) inherits tracking
|
|
221
|
+
else if (node.init.arguments.some(a => a.type === 'Identifier' && ctx.workflowPathVars.has(a.name))) {
|
|
222
|
+
ctx.workflowPathVars.add(node.id.name);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function handleCallExpression(node, ctx) {
|
|
230
|
+
const callName = getCallName(node);
|
|
231
|
+
|
|
232
|
+
// Detect require() with non-literal argument (obfuscation)
|
|
233
|
+
if (callName === 'require' && node.arguments.length > 0) {
|
|
234
|
+
const arg = node.arguments[0];
|
|
235
|
+
if (arg.type === 'BinaryExpression' && arg.operator === '+') {
|
|
236
|
+
ctx.threats.push({
|
|
237
|
+
type: 'dynamic_require',
|
|
238
|
+
severity: 'HIGH',
|
|
239
|
+
message: 'Dynamic require() with string concatenation (module name obfuscation).',
|
|
240
|
+
file: ctx.relFile
|
|
241
|
+
});
|
|
242
|
+
} else if (arg.type === 'TemplateLiteral' && arg.expressions.length > 0) {
|
|
243
|
+
ctx.threats.push({
|
|
244
|
+
type: 'dynamic_require',
|
|
245
|
+
severity: 'HIGH',
|
|
246
|
+
message: 'Dynamic require() with template literal (module name obfuscation).',
|
|
247
|
+
file: ctx.relFile
|
|
248
|
+
});
|
|
249
|
+
} else if (arg.type === 'CallExpression') {
|
|
250
|
+
const argCallName = getCallName(arg);
|
|
251
|
+
// Skip safe patterns: require(path.join(...)), require(path.resolve(...))
|
|
252
|
+
if (argCallName !== 'path.join' && argCallName !== 'path.resolve') {
|
|
253
|
+
const hasDecode = containsDecodePattern(arg);
|
|
254
|
+
ctx.threats.push({
|
|
255
|
+
type: 'dynamic_require',
|
|
256
|
+
severity: hasDecode ? 'CRITICAL' : 'HIGH',
|
|
257
|
+
message: hasDecode
|
|
258
|
+
? 'Dynamic require() with runtime decode (base64/atob obfuscation).'
|
|
259
|
+
: 'Dynamic require() with computed argument (possible decode obfuscation).',
|
|
260
|
+
file: ctx.relFile
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
} else if (arg.type === 'Identifier') {
|
|
264
|
+
ctx.threats.push({
|
|
265
|
+
type: 'dynamic_require',
|
|
266
|
+
severity: 'HIGH',
|
|
267
|
+
message: 'Dynamic require() with variable argument (module name obfuscation).',
|
|
268
|
+
file: ctx.relFile
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Detect exec/execSync with dangerous shell commands (direct or via MemberExpression)
|
|
274
|
+
const execName = callName === 'exec' || callName === 'execSync' ? callName : null;
|
|
275
|
+
const memberExec = !execName && node.callee.type === 'MemberExpression' &&
|
|
276
|
+
node.callee.property?.type === 'Identifier' &&
|
|
277
|
+
(node.callee.property.name === 'exec' || node.callee.property.name === 'execSync')
|
|
278
|
+
? node.callee.property.name : null;
|
|
279
|
+
if ((execName || memberExec) && node.arguments.length > 0) {
|
|
280
|
+
const arg = node.arguments[0];
|
|
281
|
+
let cmdStr = null;
|
|
282
|
+
if (arg.type === 'Literal' && typeof arg.value === 'string') {
|
|
283
|
+
cmdStr = arg.value;
|
|
284
|
+
} else if (arg.type === 'Identifier' && ctx.dangerousCmdVars.has(arg.name)) {
|
|
285
|
+
// Variable was assigned a dangerous command string
|
|
286
|
+
cmdStr = ctx.dangerousCmdVars.get(arg.name);
|
|
287
|
+
} else if (arg.type === 'Identifier' && ctx.execPathVars.has(arg.name)) {
|
|
288
|
+
// Variable was assigned a temp/executable file path
|
|
289
|
+
cmdStr = ctx.execPathVars.get(arg.name);
|
|
290
|
+
} else if (arg.type === 'TemplateLiteral') {
|
|
291
|
+
cmdStr = arg.quasis.map(q => q.value.raw).join('***');
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
if (cmdStr) {
|
|
295
|
+
// Check for dangerous shell patterns
|
|
296
|
+
if (/\|\s*(sh|bash)\b/.test(cmdStr) || /nc\s+-[elp]/.test(cmdStr) || /\/dev\/tcp\//.test(cmdStr) || /bash\s+-i/.test(cmdStr) || /\bcurl\b/.test(cmdStr) || /\bwget\b/.test(cmdStr)) {
|
|
297
|
+
ctx.threats.push({
|
|
298
|
+
type: 'dangerous_exec',
|
|
299
|
+
severity: 'CRITICAL',
|
|
300
|
+
message: `Dangerous shell command in exec(): "${cmdStr.substring(0, 80)}"`,
|
|
301
|
+
file: ctx.relFile
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Check for temp file execution (binary dropper pattern)
|
|
306
|
+
if (/^\/tmp\/|^\/var\/tmp\//i.test(cmdStr)) {
|
|
307
|
+
ctx.threats.push({
|
|
308
|
+
type: 'dangerous_exec',
|
|
309
|
+
severity: 'CRITICAL',
|
|
310
|
+
message: `Execution of temp file "${cmdStr.substring(0, 80)}" — binary dropper pattern.`,
|
|
311
|
+
file: ctx.relFile
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// Check for credential-stealing CLI commands
|
|
316
|
+
for (const credCmd of CREDENTIAL_CLI_COMMANDS) {
|
|
317
|
+
if (cmdStr.includes(credCmd)) {
|
|
318
|
+
ctx.threats.push({
|
|
319
|
+
type: 'credential_command_exec',
|
|
320
|
+
severity: 'CRITICAL',
|
|
321
|
+
message: `Credential theft via CLI tool: exec("${credCmd}") — steals auth tokens from installed tools.`,
|
|
322
|
+
file: ctx.relFile
|
|
323
|
+
});
|
|
324
|
+
break;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// Detect exec/execSync called on a dynamically-required module variable
|
|
331
|
+
if ((execName || memberExec) && node.callee.type === 'MemberExpression' && node.callee.object?.type === 'Identifier') {
|
|
332
|
+
if (ctx.dynamicRequireVars.has(node.callee.object.name)) {
|
|
333
|
+
const method = execName || memberExec;
|
|
334
|
+
ctx.threats.push({
|
|
335
|
+
type: 'dynamic_require_exec',
|
|
336
|
+
severity: 'CRITICAL',
|
|
337
|
+
message: `${method}() called on dynamically-required module "${node.callee.object.name}" — obfuscated command execution.`,
|
|
338
|
+
file: ctx.relFile
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Detect chained: require(non-literal).exec(...) — direct dynamic require + exec
|
|
344
|
+
if ((execName || memberExec) && node.callee.type === 'MemberExpression' &&
|
|
345
|
+
node.callee.object?.type === 'CallExpression') {
|
|
346
|
+
const innerCall = node.callee.object;
|
|
347
|
+
const innerName = getCallName(innerCall);
|
|
348
|
+
if (innerName === 'require' && innerCall.arguments.length > 0 &&
|
|
349
|
+
innerCall.arguments[0]?.type !== 'Literal') {
|
|
350
|
+
const method = execName || memberExec;
|
|
351
|
+
ctx.threats.push({
|
|
352
|
+
type: 'dynamic_require_exec',
|
|
353
|
+
severity: 'CRITICAL',
|
|
354
|
+
message: `${method}() chained on dynamic require() — obfuscated module + command execution.`,
|
|
355
|
+
file: ctx.relFile
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// Detect sandbox/container evasion
|
|
361
|
+
if (node.callee.type === 'MemberExpression' && node.callee.property?.type === 'Identifier') {
|
|
362
|
+
const fsMethod = node.callee.property.name;
|
|
363
|
+
if (['accessSync', 'existsSync', 'statSync', 'lstatSync', 'access', 'stat'].includes(fsMethod)) {
|
|
364
|
+
const arg = node.arguments[0];
|
|
365
|
+
if (arg?.type === 'Literal' && typeof arg.value === 'string') {
|
|
366
|
+
if (SANDBOX_INDICATORS.some(ind => arg.value.includes(ind))) {
|
|
367
|
+
ctx.threats.push({
|
|
368
|
+
type: 'sandbox_evasion',
|
|
369
|
+
severity: 'HIGH',
|
|
370
|
+
message: `Sandbox/container detection via ${fsMethod}("${arg.value}") — anti-analysis technique.`,
|
|
371
|
+
file: ctx.relFile
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// Detect spawn/execFile of shell processes
|
|
379
|
+
if ((callName === 'spawn' || callName === 'execFile') && node.arguments.length >= 1) {
|
|
380
|
+
const shellArg = node.arguments[0];
|
|
381
|
+
if (shellArg.type === 'Literal' && typeof shellArg.value === 'string') {
|
|
382
|
+
const shellBin = shellArg.value.toLowerCase();
|
|
383
|
+
if (['/bin/sh', '/bin/bash', 'sh', 'bash', 'cmd.exe', 'powershell', 'pwsh', 'cmd'].includes(shellBin)) {
|
|
384
|
+
ctx.threats.push({
|
|
385
|
+
type: 'dangerous_call_exec',
|
|
386
|
+
severity: 'MEDIUM',
|
|
387
|
+
message: `${callName}('${shellArg.value}') — direct shell process spawn detected.`,
|
|
388
|
+
file: ctx.relFile
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
// Also check when shell is computed via os.platform() ternary
|
|
393
|
+
if (shellArg.type === 'ConditionalExpression') {
|
|
394
|
+
const checkLiteral = (n) => n.type === 'Literal' && typeof n.value === 'string' &&
|
|
395
|
+
['/bin/sh', '/bin/bash', 'sh', 'bash', 'cmd.exe', 'powershell', 'pwsh', 'cmd'].includes(n.value.toLowerCase());
|
|
396
|
+
if (checkLiteral(shellArg.consequent) || checkLiteral(shellArg.alternate)) {
|
|
397
|
+
ctx.threats.push({
|
|
398
|
+
type: 'dangerous_call_exec',
|
|
399
|
+
severity: 'MEDIUM',
|
|
400
|
+
message: `${callName}() with conditional shell binary (platform-aware) — direct shell process spawn detected.`,
|
|
401
|
+
file: ctx.relFile
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// Detect spawn/fork with {detached: true}
|
|
408
|
+
if ((callName === 'spawn' || callName === 'fork') && node.arguments.length >= 2) {
|
|
409
|
+
const lastArg = node.arguments[node.arguments.length - 1];
|
|
410
|
+
if (lastArg.type === 'ObjectExpression') {
|
|
411
|
+
const hasDetached = lastArg.properties.some(p =>
|
|
412
|
+
p.key?.type === 'Identifier' && p.key.name === 'detached' &&
|
|
413
|
+
p.value?.type === 'Literal' && p.value.value === true
|
|
414
|
+
);
|
|
415
|
+
if (hasDetached) {
|
|
416
|
+
ctx.threats.push({
|
|
417
|
+
type: 'detached_process',
|
|
418
|
+
severity: 'HIGH',
|
|
419
|
+
message: `${callName}() with {detached: true} — background process survives parent exit (evasion technique).`,
|
|
420
|
+
file: ctx.relFile
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// Detect fs.writeFileSync/writeFile to .github/workflows
|
|
427
|
+
if (node.callee.type === 'MemberExpression' && node.callee.property?.type === 'Identifier') {
|
|
428
|
+
const writeMethod = node.callee.property.name;
|
|
429
|
+
if (['writeFileSync', 'writeFile'].includes(writeMethod) && node.arguments.length > 0) {
|
|
430
|
+
const pathArg = node.arguments[0];
|
|
431
|
+
const pathStr = extractStringValue(pathArg);
|
|
432
|
+
let joinedPath = null;
|
|
433
|
+
let hasWorkflowVar = false;
|
|
434
|
+
if (pathArg?.type === 'CallExpression' && pathArg.arguments) {
|
|
435
|
+
joinedPath = pathArg.arguments.map(a => {
|
|
436
|
+
if (a.type === 'Identifier' && ctx.workflowPathVars.has(a.name)) {
|
|
437
|
+
hasWorkflowVar = true;
|
|
438
|
+
return '.github/workflows';
|
|
439
|
+
}
|
|
440
|
+
return extractStringValue(a) || '';
|
|
441
|
+
}).join('/');
|
|
442
|
+
}
|
|
443
|
+
if (pathArg?.type === 'Identifier' && ctx.workflowPathVars.has(pathArg.name)) {
|
|
444
|
+
hasWorkflowVar = true;
|
|
445
|
+
}
|
|
446
|
+
const checkPath = pathStr || joinedPath || '';
|
|
447
|
+
if (hasWorkflowVar || /\.github[\\/]workflows/i.test(checkPath) || /\.github[\\/]actions/i.test(checkPath)) {
|
|
448
|
+
ctx.threats.push({
|
|
449
|
+
type: 'workflow_write',
|
|
450
|
+
severity: 'CRITICAL',
|
|
451
|
+
message: `${writeMethod}() writes to .github/workflows — GitHub Actions injection/persistence technique.`,
|
|
452
|
+
file: ctx.relFile
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// Detect fs.mkdirSync creating .github/workflows
|
|
459
|
+
if (node.callee.type === 'MemberExpression' && node.callee.property?.type === 'Identifier') {
|
|
460
|
+
const mkdirMethod = node.callee.property.name;
|
|
461
|
+
if ((mkdirMethod === 'mkdirSync' || mkdirMethod === 'mkdir') && node.arguments.length > 0) {
|
|
462
|
+
const pathArg = node.arguments[0];
|
|
463
|
+
if (pathArg?.type === 'Identifier' && ctx.workflowPathVars.has(pathArg.name)) {
|
|
464
|
+
ctx.threats.push({
|
|
465
|
+
type: 'workflow_write',
|
|
466
|
+
severity: 'CRITICAL',
|
|
467
|
+
message: `${mkdirMethod}() creates .github/workflows directory — GitHub Actions persistence technique.`,
|
|
468
|
+
file: ctx.relFile
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// Detect fs.readdirSync on .github/workflows
|
|
475
|
+
if (node.callee.type === 'MemberExpression' && node.callee.property?.type === 'Identifier') {
|
|
476
|
+
const readDirMethod = node.callee.property.name;
|
|
477
|
+
if ((readDirMethod === 'readdirSync' || readDirMethod === 'readdir') && node.arguments.length > 0) {
|
|
478
|
+
const pathArg = node.arguments[0];
|
|
479
|
+
if (pathArg?.type === 'Identifier' && ctx.workflowPathVars.has(pathArg.name)) {
|
|
480
|
+
ctx.threats.push({
|
|
481
|
+
type: 'workflow_write',
|
|
482
|
+
severity: 'CRITICAL',
|
|
483
|
+
message: `${readDirMethod}() enumerates .github/workflows — workflow modification/injection technique.`,
|
|
484
|
+
file: ctx.relFile
|
|
485
|
+
});
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// Detect fs.chmodSync with executable permissions
|
|
491
|
+
if (node.callee.type === 'MemberExpression' && node.callee.property?.type === 'Identifier') {
|
|
492
|
+
const chmodMethod = node.callee.property.name;
|
|
493
|
+
if ((chmodMethod === 'chmodSync' || chmodMethod === 'chmod') && node.arguments.length >= 2) {
|
|
494
|
+
const modeArg = node.arguments[1];
|
|
495
|
+
if (modeArg?.type === 'Literal' && typeof modeArg.value === 'number') {
|
|
496
|
+
// 0o755=493, 0o777=511, 0o700=448, 0o775=509
|
|
497
|
+
if (modeArg.value === 493 || modeArg.value === 511 || modeArg.value === 448 || modeArg.value === 509) {
|
|
498
|
+
ctx.threats.push({
|
|
499
|
+
type: 'binary_dropper',
|
|
500
|
+
severity: 'CRITICAL',
|
|
501
|
+
message: `${chmodMethod}() with executable permissions (0o${modeArg.value.toString(8)}) — binary dropper pattern.`,
|
|
502
|
+
file: ctx.relFile
|
|
503
|
+
});
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
// Detect AI agent weaponization
|
|
510
|
+
if ((callName === 'spawn' || callName === 'exec' || callName === 'execSync' ||
|
|
511
|
+
callName === 'execFile' || callName === 'execFileSync' || memberExec) &&
|
|
512
|
+
node.arguments.length > 0) {
|
|
513
|
+
const argStrings = [];
|
|
514
|
+
for (const arg of node.arguments) {
|
|
515
|
+
if (arg.type === 'Literal' && typeof arg.value === 'string') {
|
|
516
|
+
argStrings.push(arg.value);
|
|
517
|
+
} else if (arg.type === 'ArrayExpression') {
|
|
518
|
+
for (const el of arg.elements) {
|
|
519
|
+
if (el && el.type === 'Literal' && typeof el.value === 'string') {
|
|
520
|
+
argStrings.push(el.value);
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
const allArgText = argStrings.join(' ');
|
|
527
|
+
const hasDangerousFlag = AI_AGENT_DANGEROUS_FLAGS.some(flag => allArgText.includes(flag));
|
|
528
|
+
const firstArg = node.arguments[0];
|
|
529
|
+
const cmdName = firstArg?.type === 'Literal' && typeof firstArg.value === 'string'
|
|
530
|
+
? firstArg.value.toLowerCase() : '';
|
|
531
|
+
const isAIAgent = AI_AGENT_BINARIES.some(bin => cmdName === bin || cmdName.endsWith('/' + bin));
|
|
532
|
+
|
|
533
|
+
if (hasDangerousFlag) {
|
|
534
|
+
const matchedFlag = AI_AGENT_DANGEROUS_FLAGS.find(flag => allArgText.includes(flag));
|
|
535
|
+
ctx.threats.push({
|
|
536
|
+
type: 'ai_agent_abuse',
|
|
537
|
+
severity: 'CRITICAL',
|
|
538
|
+
message: `AI agent invoked with security bypass flag "${matchedFlag}"${isAIAgent ? ` (agent: ${cmdName})` : ''} — weaponized AI coding assistant (s1ngularity/Nx pattern).`,
|
|
539
|
+
file: ctx.relFile
|
|
540
|
+
});
|
|
541
|
+
} else if (isAIAgent) {
|
|
542
|
+
ctx.threats.push({
|
|
543
|
+
type: 'ai_agent_abuse',
|
|
544
|
+
severity: 'HIGH',
|
|
545
|
+
message: `AI coding agent "${cmdName}" invoked from package — potential AI agent weaponization.`,
|
|
546
|
+
file: ctx.relFile
|
|
547
|
+
});
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
// Detect Object.defineProperty(process.env, ...) — env interception
|
|
552
|
+
if (node.callee.type === 'MemberExpression' &&
|
|
553
|
+
node.callee.object?.type === 'Identifier' && node.callee.object.name === 'Object' &&
|
|
554
|
+
node.callee.property?.type === 'Identifier' && node.callee.property.name === 'defineProperty' &&
|
|
555
|
+
node.arguments.length >= 2) {
|
|
556
|
+
const target = node.arguments[0];
|
|
557
|
+
if (target.type === 'MemberExpression' &&
|
|
558
|
+
target.object?.name === 'process' &&
|
|
559
|
+
target.property?.name === 'env') {
|
|
560
|
+
ctx.threats.push({
|
|
561
|
+
type: 'env_proxy_intercept',
|
|
562
|
+
severity: 'CRITICAL',
|
|
563
|
+
message: 'Object.defineProperty(process.env) detected — intercepts environment variable access for credential theft.',
|
|
564
|
+
file: ctx.relFile
|
|
565
|
+
});
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
if (callName === 'eval') {
|
|
570
|
+
ctx.hasEvalInFile = true;
|
|
571
|
+
// Detect staged eval decode
|
|
572
|
+
if (node.arguments.length === 1 && hasDecodeArg(node.arguments[0])) {
|
|
573
|
+
ctx.threats.push({
|
|
574
|
+
type: 'staged_eval_decode',
|
|
575
|
+
severity: 'CRITICAL',
|
|
576
|
+
message: 'eval() with decode argument (atob/Buffer.from base64) — staged payload execution.',
|
|
577
|
+
file: ctx.relFile
|
|
578
|
+
});
|
|
579
|
+
} else {
|
|
580
|
+
const isConstant = hasOnlyStringLiteralArgs(node);
|
|
581
|
+
ctx.threats.push({
|
|
582
|
+
type: 'dangerous_call_eval',
|
|
583
|
+
severity: isConstant ? 'LOW' : 'HIGH',
|
|
584
|
+
message: isConstant
|
|
585
|
+
? 'eval() with constant string literal (low risk, globalThis polyfill pattern).'
|
|
586
|
+
: 'Dangerous call "eval" with dynamic expression detected.',
|
|
587
|
+
file: ctx.relFile
|
|
588
|
+
});
|
|
589
|
+
}
|
|
590
|
+
} else if (callName === 'Function') {
|
|
591
|
+
// Detect staged Function decode
|
|
592
|
+
if (node.arguments.length >= 1 && hasDecodeArg(node.arguments[node.arguments.length - 1])) {
|
|
593
|
+
ctx.threats.push({
|
|
594
|
+
type: 'staged_eval_decode',
|
|
595
|
+
severity: 'CRITICAL',
|
|
596
|
+
message: 'Function() with decode argument (atob/Buffer.from base64) — staged payload execution.',
|
|
597
|
+
file: ctx.relFile
|
|
598
|
+
});
|
|
599
|
+
} else {
|
|
600
|
+
const isConstant = hasOnlyStringLiteralArgs(node);
|
|
601
|
+
ctx.threats.push({
|
|
602
|
+
type: 'dangerous_call_function',
|
|
603
|
+
severity: isConstant ? 'LOW' : 'MEDIUM',
|
|
604
|
+
message: isConstant
|
|
605
|
+
? 'Function() with constant string literal (low risk, globalThis polyfill pattern).'
|
|
606
|
+
: 'Function() with dynamic expression (template/factory pattern).',
|
|
607
|
+
file: ctx.relFile
|
|
608
|
+
});
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
// Detect indirect eval/Function via computed property
|
|
613
|
+
if (node.callee.type === 'MemberExpression' && node.callee.computed) {
|
|
614
|
+
const prop = node.callee.property;
|
|
615
|
+
if (prop.type === 'Literal' && typeof prop.value === 'string') {
|
|
616
|
+
if (prop.value === 'eval') {
|
|
617
|
+
ctx.hasEvalInFile = true;
|
|
618
|
+
ctx.threats.push({
|
|
619
|
+
type: 'dangerous_call_eval',
|
|
620
|
+
severity: 'HIGH',
|
|
621
|
+
message: 'Indirect eval via computed property access (obj["eval"]) — evasion technique.',
|
|
622
|
+
file: ctx.relFile
|
|
623
|
+
});
|
|
624
|
+
} else if (prop.value === 'Function') {
|
|
625
|
+
ctx.threats.push({
|
|
626
|
+
type: 'dangerous_call_function',
|
|
627
|
+
severity: 'MEDIUM',
|
|
628
|
+
message: 'Indirect Function via computed property access (obj["Function"]) — evasion technique.',
|
|
629
|
+
file: ctx.relFile
|
|
630
|
+
});
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
// Detect computed call on globalThis/global alias with variable property
|
|
634
|
+
const obj = node.callee.object;
|
|
635
|
+
if (prop.type === 'Identifier' && obj?.type === 'Identifier' &&
|
|
636
|
+
(ctx.globalThisAliases.has(obj.name) || obj.name === 'globalThis' || obj.name === 'global')) {
|
|
637
|
+
ctx.hasEvalInFile = true;
|
|
638
|
+
ctx.threats.push({
|
|
639
|
+
type: 'dangerous_call_eval',
|
|
640
|
+
severity: 'HIGH',
|
|
641
|
+
message: `Dynamic global dispatch via computed property (${obj.name}[${prop.name}]) — likely indirect eval evasion.`,
|
|
642
|
+
file: ctx.relFile
|
|
643
|
+
});
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
// Detect indirect eval/Function via sequence expression
|
|
648
|
+
if (node.callee.type === 'SequenceExpression') {
|
|
649
|
+
const exprs = node.callee.expressions;
|
|
650
|
+
const last = exprs[exprs.length - 1];
|
|
651
|
+
if (last && last.type === 'Identifier') {
|
|
652
|
+
if (last.name === 'eval') {
|
|
653
|
+
ctx.hasEvalInFile = true;
|
|
654
|
+
ctx.threats.push({
|
|
655
|
+
type: 'dangerous_call_eval',
|
|
656
|
+
severity: 'HIGH',
|
|
657
|
+
message: 'Indirect eval via sequence expression ((0, eval)) — evasion technique.',
|
|
658
|
+
file: ctx.relFile
|
|
659
|
+
});
|
|
660
|
+
} else if (last.name === 'Function') {
|
|
661
|
+
ctx.threats.push({
|
|
662
|
+
type: 'dangerous_call_function',
|
|
663
|
+
severity: 'MEDIUM',
|
|
664
|
+
message: 'Indirect Function via sequence expression ((0, Function)) — evasion technique.',
|
|
665
|
+
file: ctx.relFile
|
|
666
|
+
});
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
// Detect crypto.createDecipher/createDecipheriv and module._compile
|
|
672
|
+
if (node.callee.type === 'MemberExpression') {
|
|
673
|
+
const prop = node.callee.property;
|
|
674
|
+
const propName = prop.type === 'Identifier' ? prop.name :
|
|
675
|
+
(prop.type === 'Literal' ? prop.value : null);
|
|
676
|
+
if (propName === 'createDecipher' || propName === 'createDecipheriv') {
|
|
677
|
+
ctx.threats.push({
|
|
678
|
+
type: 'crypto_decipher',
|
|
679
|
+
severity: 'HIGH',
|
|
680
|
+
message: `${propName}() detected — runtime decryption of embedded payload (event-stream/flatmap-stream pattern).`,
|
|
681
|
+
file: ctx.relFile
|
|
682
|
+
});
|
|
683
|
+
}
|
|
684
|
+
if (propName === '_compile') {
|
|
685
|
+
ctx.threats.push({
|
|
686
|
+
type: 'module_compile',
|
|
687
|
+
severity: 'CRITICAL',
|
|
688
|
+
message: 'module._compile() detected — executes arbitrary code from string in module context (flatmap-stream pattern).',
|
|
689
|
+
file: ctx.relFile
|
|
690
|
+
});
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
function handleImportExpression(node, ctx) {
|
|
696
|
+
if (node.source) {
|
|
697
|
+
const src = node.source;
|
|
698
|
+
if (src.type === 'Literal' && typeof src.value === 'string') {
|
|
699
|
+
const dangerousModules = ['child_process', 'fs', 'http', 'https', 'net', 'dns', 'tls'];
|
|
700
|
+
if (dangerousModules.includes(src.value)) {
|
|
701
|
+
ctx.threats.push({
|
|
702
|
+
type: 'dynamic_import',
|
|
703
|
+
severity: 'HIGH',
|
|
704
|
+
message: `Dynamic import() of dangerous module "${src.value}".`,
|
|
705
|
+
file: ctx.relFile
|
|
706
|
+
});
|
|
707
|
+
}
|
|
708
|
+
} else {
|
|
709
|
+
ctx.threats.push({
|
|
710
|
+
type: 'dynamic_import',
|
|
711
|
+
severity: 'HIGH',
|
|
712
|
+
message: 'Dynamic import() with computed argument (possible obfuscation).',
|
|
713
|
+
file: ctx.relFile
|
|
714
|
+
});
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
function handleNewExpression(node, ctx) {
|
|
720
|
+
if (node.callee.type === 'Identifier' && node.callee.name === 'Function') {
|
|
721
|
+
const isConstant = hasOnlyStringLiteralArgs(node);
|
|
722
|
+
ctx.threats.push({
|
|
723
|
+
type: 'dangerous_call_function',
|
|
724
|
+
severity: isConstant ? 'LOW' : 'MEDIUM',
|
|
725
|
+
message: isConstant
|
|
726
|
+
? 'new Function() with constant string literal (low risk, globalThis polyfill pattern).'
|
|
727
|
+
: 'new Function() with dynamic expression (template/factory pattern).',
|
|
728
|
+
file: ctx.relFile
|
|
729
|
+
});
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
// Detect new Proxy(process.env, handler)
|
|
733
|
+
if (node.callee.type === 'Identifier' && node.callee.name === 'Proxy' && node.arguments.length >= 2) {
|
|
734
|
+
const target = node.arguments[0];
|
|
735
|
+
if (target.type === 'MemberExpression' &&
|
|
736
|
+
target.object?.name === 'process' &&
|
|
737
|
+
target.property?.name === 'env') {
|
|
738
|
+
ctx.threats.push({
|
|
739
|
+
type: 'env_proxy_intercept',
|
|
740
|
+
severity: 'CRITICAL',
|
|
741
|
+
message: 'new Proxy(process.env) detected — intercepts all environment variable access.',
|
|
742
|
+
file: ctx.relFile
|
|
743
|
+
});
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
function handleLiteral(node, ctx) {
|
|
749
|
+
if (typeof node.value === 'string') {
|
|
750
|
+
// Ignore safe strings
|
|
751
|
+
if (SAFE_STRINGS.some(s => node.value.includes(s))) {
|
|
752
|
+
return;
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
for (const sensitive of SENSITIVE_STRINGS) {
|
|
756
|
+
if (node.value.includes(sensitive)) {
|
|
757
|
+
ctx.threats.push({
|
|
758
|
+
type: 'sensitive_string',
|
|
759
|
+
severity: 'HIGH',
|
|
760
|
+
message: `Reference to "${sensitive}" detected.`,
|
|
761
|
+
file: ctx.relFile
|
|
762
|
+
});
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
// Detect AI agent dangerous flags as string literals
|
|
767
|
+
for (const flag of AI_AGENT_DANGEROUS_FLAGS) {
|
|
768
|
+
if (node.value === flag) {
|
|
769
|
+
ctx.threats.push({
|
|
770
|
+
type: 'ai_agent_abuse',
|
|
771
|
+
severity: 'CRITICAL',
|
|
772
|
+
message: `AI agent security bypass flag "${flag}" found — weaponized AI coding assistant (s1ngularity/Nx pattern).`,
|
|
773
|
+
file: ctx.relFile
|
|
774
|
+
});
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
function handleAssignmentExpression(node, ctx) {
|
|
781
|
+
if (node.left?.type === 'MemberExpression') {
|
|
782
|
+
const left = node.left;
|
|
783
|
+
|
|
784
|
+
// globalThis.fetch = ... or globalThis.XMLHttpRequest = ...
|
|
785
|
+
if (left.object?.type === 'Identifier' && left.object.name === 'globalThis' &&
|
|
786
|
+
left.property?.type === 'Identifier') {
|
|
787
|
+
if (HOOKABLE_NATIVES.includes(left.property.name)) {
|
|
788
|
+
ctx.threats.push({
|
|
789
|
+
type: 'prototype_hook',
|
|
790
|
+
severity: 'HIGH',
|
|
791
|
+
message: `globalThis.${left.property.name} overridden — native API hooking for traffic interception.`,
|
|
792
|
+
file: ctx.relFile
|
|
793
|
+
});
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
// XMLHttpRequest.prototype.send = ... or Response.prototype.json = ...
|
|
798
|
+
if (left.object?.type === 'MemberExpression' &&
|
|
799
|
+
left.object.property?.type === 'Identifier' &&
|
|
800
|
+
left.object.property.name === 'prototype' &&
|
|
801
|
+
left.object.object?.type === 'Identifier') {
|
|
802
|
+
if (HOOKABLE_NATIVES.includes(left.object.object.name)) {
|
|
803
|
+
ctx.threats.push({
|
|
804
|
+
type: 'prototype_hook',
|
|
805
|
+
severity: 'HIGH',
|
|
806
|
+
message: `${left.object.object.name}.prototype.${left.property?.name || '?'} overridden — native API hooking for traffic interception.`,
|
|
807
|
+
file: ctx.relFile
|
|
808
|
+
});
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
// http.request = ... or https.get = ...
|
|
813
|
+
if (left.object?.type === 'Identifier' &&
|
|
814
|
+
['http', 'https'].includes(left.object.name) &&
|
|
815
|
+
left.property?.type === 'Identifier' &&
|
|
816
|
+
['request', 'get', 'createServer'].includes(left.property.name) &&
|
|
817
|
+
node.right?.type === 'FunctionExpression') {
|
|
818
|
+
ctx.threats.push({
|
|
819
|
+
type: 'prototype_hook',
|
|
820
|
+
severity: 'HIGH',
|
|
821
|
+
message: `${left.object.name}.${left.property.name} overridden — Node.js network module hooking for traffic interception.`,
|
|
822
|
+
file: ctx.relFile
|
|
823
|
+
});
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
// <module>.<Class>.prototype.<method> = ...
|
|
827
|
+
if (left.object?.type === 'MemberExpression' &&
|
|
828
|
+
left.object.property?.type === 'Identifier' && left.object.property.name === 'prototype' &&
|
|
829
|
+
left.object.object?.type === 'MemberExpression' &&
|
|
830
|
+
left.object.object.object?.type === 'Identifier' &&
|
|
831
|
+
left.object.object.property?.type === 'Identifier') {
|
|
832
|
+
const moduleName = left.object.object.object.name;
|
|
833
|
+
const className = left.object.object.property.name;
|
|
834
|
+
if (NODE_HOOKABLE_MODULES.includes(moduleName) && NODE_HOOKABLE_CLASSES.includes(className)) {
|
|
835
|
+
ctx.threats.push({
|
|
836
|
+
type: 'prototype_hook',
|
|
837
|
+
severity: 'CRITICAL',
|
|
838
|
+
message: `${moduleName}.${className}.prototype.${left.property?.name || '?'} overridden — Node.js core module prototype hooking for traffic interception.`,
|
|
839
|
+
file: ctx.relFile
|
|
840
|
+
});
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
function handleMemberExpression(node, ctx) {
|
|
847
|
+
// Detect require.cache access
|
|
848
|
+
if (node.object?.type === 'Identifier' && node.object.name === 'require' &&
|
|
849
|
+
node.property?.type === 'Identifier' && node.property.name === 'cache') {
|
|
850
|
+
ctx.threats.push({
|
|
851
|
+
type: 'require_cache_poison',
|
|
852
|
+
severity: 'CRITICAL',
|
|
853
|
+
message: 'require.cache accessed — module cache poisoning to hijack or replace core Node.js modules.',
|
|
854
|
+
file: ctx.relFile
|
|
855
|
+
});
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
if (
|
|
859
|
+
node.object?.object?.name === 'process' &&
|
|
860
|
+
node.object?.property?.name === 'env'
|
|
861
|
+
) {
|
|
862
|
+
// Dynamic access: process.env[variable]
|
|
863
|
+
if (node.computed) {
|
|
864
|
+
if (ctx.hasFromCharCode) {
|
|
865
|
+
ctx.threats.push({
|
|
866
|
+
type: 'env_charcode_reconstruction',
|
|
867
|
+
severity: 'HIGH',
|
|
868
|
+
message: 'process.env accessed with dynamically reconstructed key (String.fromCharCode obfuscation).',
|
|
869
|
+
file: ctx.relFile
|
|
870
|
+
});
|
|
871
|
+
}
|
|
872
|
+
ctx.threats.push({
|
|
873
|
+
type: 'env_access',
|
|
874
|
+
severity: 'MEDIUM',
|
|
875
|
+
message: 'Dynamic access to process.env (variable key).',
|
|
876
|
+
file: ctx.relFile
|
|
877
|
+
});
|
|
878
|
+
return;
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
const envVar = node.property?.name;
|
|
882
|
+
if (envVar) {
|
|
883
|
+
if (SAFE_ENV_VARS.includes(envVar)) {
|
|
884
|
+
return;
|
|
885
|
+
}
|
|
886
|
+
const envLower = envVar.toLowerCase();
|
|
887
|
+
if (SAFE_ENV_PREFIXES.some(p => envLower.startsWith(p))) {
|
|
888
|
+
return;
|
|
889
|
+
}
|
|
890
|
+
if (ENV_SENSITIVE_KEYWORDS.some(s => envVar.toUpperCase().includes(s))) {
|
|
891
|
+
ctx.threats.push({
|
|
892
|
+
type: 'env_access',
|
|
893
|
+
severity: 'HIGH',
|
|
894
|
+
message: `Access to sensitive variable process.env.${envVar}.`,
|
|
895
|
+
file: ctx.relFile
|
|
896
|
+
});
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
function handlePostWalk(ctx) {
|
|
903
|
+
// JS reverse shell pattern
|
|
904
|
+
if (ctx.hasJsReverseShell) {
|
|
905
|
+
ctx.threats.push({
|
|
906
|
+
type: 'reverse_shell',
|
|
907
|
+
severity: 'CRITICAL',
|
|
908
|
+
message: 'JavaScript reverse shell: net.Socket + connect() + pipe to shell process stdin/stdout.',
|
|
909
|
+
file: ctx.relFile
|
|
910
|
+
});
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
// Steganographic/binary payload execution
|
|
914
|
+
if (ctx.hasBinaryFileLiteral && ctx.hasEvalInFile) {
|
|
915
|
+
ctx.threats.push({
|
|
916
|
+
type: 'staged_binary_payload',
|
|
917
|
+
severity: 'HIGH',
|
|
918
|
+
message: 'Binary file reference (.png/.jpg/.wasm/etc.) + eval() in same file — possible steganographic payload execution.',
|
|
919
|
+
file: ctx.relFile
|
|
920
|
+
});
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
module.exports = {
|
|
925
|
+
handleVariableDeclarator,
|
|
926
|
+
handleCallExpression,
|
|
927
|
+
handleImportExpression,
|
|
928
|
+
handleNewExpression,
|
|
929
|
+
handleLiteral,
|
|
930
|
+
handleAssignmentExpression,
|
|
931
|
+
handleMemberExpression,
|
|
932
|
+
handlePostWalk
|
|
933
|
+
};
|