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.
@@ -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
+ };