muaddib-scanner 2.1.5 → 2.2.0

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.
Files changed (100) hide show
  1. package/README.fr.md +33 -8
  2. package/README.md +33 -8
  3. package/assets/logo2removebg.png +0 -0
  4. package/bin/muaddib.js +9 -0
  5. package/datasets/adversarial/README.md +23 -0
  6. package/datasets/adversarial/ai-agent-weaponization/index.js +5 -0
  7. package/datasets/adversarial/ai-agent-weaponization/package.json +9 -0
  8. package/datasets/adversarial/ai-agent-weaponization/setup.js +83 -0
  9. package/datasets/adversarial/ai-config-injection/.cursorrules +36 -0
  10. package/datasets/adversarial/ai-config-injection/index.js +16 -0
  11. package/datasets/adversarial/ai-config-injection/package.json +8 -0
  12. package/datasets/adversarial/browser-api-hook/index.js +66 -0
  13. package/datasets/adversarial/browser-api-hook/package.json +6 -0
  14. package/datasets/adversarial/bun-runtime-evasion/bun_environment.js +23 -0
  15. package/datasets/adversarial/bun-runtime-evasion/package.json +9 -0
  16. package/datasets/adversarial/bun-runtime-evasion/setup.js +10 -0
  17. package/datasets/adversarial/ci-trigger-exfil/index.js +17 -0
  18. package/datasets/adversarial/ci-trigger-exfil/package.json +9 -0
  19. package/datasets/adversarial/conditional-chain/index.js +14 -0
  20. package/datasets/adversarial/conditional-chain/package.json +9 -0
  21. package/datasets/adversarial/crypto-wallet-harvest/index.js +44 -0
  22. package/datasets/adversarial/crypto-wallet-harvest/package.json +6 -0
  23. package/datasets/adversarial/dead-mans-switch/index.js +35 -0
  24. package/datasets/adversarial/dead-mans-switch/package.json +9 -0
  25. package/datasets/adversarial/delayed-exfil/index.js +6 -0
  26. package/datasets/adversarial/delayed-exfil/package.json +6 -0
  27. package/datasets/adversarial/detached-background/launcher.js +11 -0
  28. package/datasets/adversarial/detached-background/package.json +9 -0
  29. package/datasets/adversarial/detached-background/worker.js +26 -0
  30. package/datasets/adversarial/discord-webhook-exfil/index.js +95 -0
  31. package/datasets/adversarial/discord-webhook-exfil/package.json +9 -0
  32. package/datasets/adversarial/dns-chunk-exfil/index.js +10 -0
  33. package/datasets/adversarial/dns-chunk-exfil/package.json +6 -0
  34. package/datasets/adversarial/docker-aware/index.js +10 -0
  35. package/datasets/adversarial/docker-aware/package.json +6 -0
  36. package/datasets/adversarial/double-base64-exfil/index.js +11 -0
  37. package/datasets/adversarial/double-base64-exfil/package.json +9 -0
  38. package/datasets/adversarial/dynamic-import/index.js +21 -0
  39. package/datasets/adversarial/dynamic-import/package.json +9 -0
  40. package/datasets/adversarial/dynamic-require/index.js +3 -0
  41. package/datasets/adversarial/dynamic-require/package.json +9 -0
  42. package/datasets/adversarial/fake-captcha-fingerprint/index.js +64 -0
  43. package/datasets/adversarial/fake-captcha-fingerprint/package.json +9 -0
  44. package/datasets/adversarial/gh-cli-token-steal/index.js +31 -0
  45. package/datasets/adversarial/gh-cli-token-steal/package.json +6 -0
  46. package/datasets/adversarial/github-exfil/index.js +33 -0
  47. package/datasets/adversarial/github-exfil/package.json +9 -0
  48. package/datasets/adversarial/iife-exfil/index.js +17 -0
  49. package/datasets/adversarial/iife-exfil/package.json +9 -0
  50. package/datasets/adversarial/nested-payload/index.js +3 -0
  51. package/datasets/adversarial/nested-payload/package.json +9 -0
  52. package/datasets/adversarial/nested-payload/utils/helper.js +6 -0
  53. package/datasets/adversarial/nested-payload/utils/lib/format.js +23 -0
  54. package/datasets/adversarial/postinstall-download/package.json +8 -0
  55. package/datasets/adversarial/preinstall-background-fork/bootstrap.js +16 -0
  56. package/datasets/adversarial/preinstall-background-fork/index.js +2 -0
  57. package/datasets/adversarial/preinstall-background-fork/package.json +9 -0
  58. package/datasets/adversarial/preinstall-background-fork/stealer.js +67 -0
  59. package/datasets/adversarial/preinstall-exec/package.json +9 -0
  60. package/datasets/adversarial/preinstall-exec/steal.js +24 -0
  61. package/datasets/adversarial/proxy-env-intercept/index.js +33 -0
  62. package/datasets/adversarial/proxy-env-intercept/package.json +9 -0
  63. package/datasets/adversarial/pyinstaller-dropper/index.js +25 -0
  64. package/datasets/adversarial/pyinstaller-dropper/package.json +9 -0
  65. package/datasets/adversarial/rdd-zero-deps/index.js +32 -0
  66. package/datasets/adversarial/rdd-zero-deps/init.js +15 -0
  67. package/datasets/adversarial/rdd-zero-deps/package.json +11 -0
  68. package/datasets/adversarial/remote-dynamic-dependency/index.js +15 -0
  69. package/datasets/adversarial/remote-dynamic-dependency/package.json +7 -0
  70. package/datasets/adversarial/self-hosted-runner-backdoor/index.js +28 -0
  71. package/datasets/adversarial/self-hosted-runner-backdoor/package.json +9 -0
  72. package/datasets/adversarial/silent-error-swallow/index.js +32 -0
  73. package/datasets/adversarial/silent-error-swallow/package.json +6 -0
  74. package/datasets/adversarial/staged-fetch/index.js +9 -0
  75. package/datasets/adversarial/staged-fetch/package.json +6 -0
  76. package/datasets/adversarial/string-concat-obfuscation/index.js +6 -0
  77. package/datasets/adversarial/string-concat-obfuscation/package.json +6 -0
  78. package/datasets/adversarial/template-literal-obfuscation/index.js +4 -0
  79. package/datasets/adversarial/template-literal-obfuscation/package.json +9 -0
  80. package/datasets/adversarial/triple-base64-github-push/index.js +38 -0
  81. package/datasets/adversarial/triple-base64-github-push/package.json +9 -0
  82. package/datasets/adversarial/websocket-exfil/index.js +34 -0
  83. package/datasets/adversarial/websocket-exfil/package.json +9 -0
  84. package/datasets/benign/README.md +20 -0
  85. package/datasets/benign/packages-npm.txt +98 -0
  86. package/datasets/benign/packages-pypi.txt +49 -0
  87. package/metrics/v2.1.5.json +753 -0
  88. package/metrics/v2.2.0.json +753 -0
  89. package/nul +0 -5
  90. package/package.json +1 -1
  91. package/src/commands/evaluate.js +270 -0
  92. package/src/index.js +12 -5
  93. package/src/ioc/bootstrap.js +1 -0
  94. package/src/response/playbooks.js +66 -0
  95. package/src/rules/index.js +181 -0
  96. package/src/scanner/ai-config.js +183 -0
  97. package/src/scanner/ast.js +496 -2
  98. package/src/scanner/dataflow.js +147 -16
  99. package/src/scanner/package.js +3 -1
  100. package/src/utils.js +10 -3
@@ -23,7 +23,9 @@ const SENSITIVE_STRINGS = [
23
23
  '.ssh',
24
24
  'Shai-Hulud',
25
25
  'The Second Coming',
26
- 'Goldox-T3chs'
26
+ 'Goldox-T3chs',
27
+ '/etc/passwd',
28
+ '/etc/shadow'
27
29
  ];
28
30
 
29
31
  // Env vars that are safe and should NOT be flagged (common config/runtime vars)
@@ -37,6 +39,18 @@ const ENV_SENSITIVE_KEYWORDS = [
37
39
  'TOKEN', 'SECRET', 'KEY', 'PASSWORD', 'CREDENTIAL', 'AUTH'
38
40
  ];
39
41
 
42
+ // AI agent dangerous flags — disable security controls (s1ngularity/Nx, Aug 2025)
43
+ const AI_AGENT_DANGEROUS_FLAGS = [
44
+ '--dangerously-skip-permissions',
45
+ '--yolo',
46
+ '--trust-all-tools',
47
+ '--yes-always',
48
+ '--no-permission-check'
49
+ ];
50
+
51
+ // AI agent binary names
52
+ const AI_AGENT_BINARIES = ['claude', 'gemini', 'q', 'aider', 'copilot', 'cursor'];
53
+
40
54
  // Strings that are NOT suspicious
41
55
  const SAFE_STRINGS = [
42
56
  'api.github.com',
@@ -44,6 +58,33 @@ const SAFE_STRINGS = [
44
58
  'npmjs.com'
45
59
  ];
46
60
 
61
+ // Credential-stealing CLI commands (s1ngularity/Nx, Shai-Hulud)
62
+ const CREDENTIAL_CLI_COMMANDS = [
63
+ 'gh auth token',
64
+ 'gcloud auth print-access-token',
65
+ 'aws sts get-session-token',
66
+ 'az account get-access-token',
67
+ 'heroku auth:token',
68
+ 'netlify api --data',
69
+ 'vercel whoami'
70
+ ];
71
+
72
+ // Dangerous shell command patterns for variable tracking
73
+ const DANGEROUS_CMD_PATTERNS = [/\bcurl\b/, /\bwget\b/, /\bnc\s+-/, /\/dev\/tcp\//, /\bbash\s+-i/];
74
+
75
+ // Native APIs targeted for prototype hooking (chalk Sept 2025, Sygnia)
76
+ const HOOKABLE_NATIVES = [
77
+ 'fetch', 'XMLHttpRequest', 'Request', 'Response',
78
+ 'WebSocket', 'EventSource'
79
+ ];
80
+
81
+ // Paths indicating sandbox/container environment detection (anti-analysis)
82
+ const SANDBOX_INDICATORS = [
83
+ '/.dockerenv',
84
+ '/proc/1/cgroup',
85
+ '/proc/self/cgroup'
86
+ ];
87
+
47
88
  async function analyzeAST(targetPath) {
48
89
  const threats = [];
49
90
  const files = findJsFiles(targetPath);
@@ -89,6 +130,27 @@ function hasOnlyStringLiteralArgs(node) {
89
130
  return node.arguments.every(arg => arg.type === 'Literal' && typeof arg.value === 'string');
90
131
  }
91
132
 
133
+ /**
134
+ * Checks if an AST subtree contains decode patterns (base64, atob, fromCharCode).
135
+ */
136
+ function containsDecodePattern(node) {
137
+ if (!node || typeof node !== 'object') return false;
138
+ if (node.type === 'Literal' && node.value === 'base64') return true;
139
+ if (node.type === 'Identifier' && (node.name === 'atob' || node.name === 'fromCharCode')) return true;
140
+ for (const key of Object.keys(node)) {
141
+ if (key === 'type' || key === 'start' || key === 'end' || key === 'loc') continue;
142
+ const child = node[key];
143
+ if (Array.isArray(child)) {
144
+ for (const item of child) {
145
+ if (item && typeof item.type === 'string' && containsDecodePattern(item)) return true;
146
+ }
147
+ } else if (child && typeof child === 'object' && typeof child.type === 'string') {
148
+ if (containsDecodePattern(child)) return true;
149
+ }
150
+ }
151
+ return false;
152
+ }
153
+
92
154
  function analyzeFile(content, filePath, basePath) {
93
155
  const threats = [];
94
156
  let ast;
@@ -111,10 +173,338 @@ function analyzeFile(content, filePath, basePath) {
111
173
  return threats;
112
174
  }
113
175
 
176
+ // Track variables assigned from dynamic require() calls (non-literal arg)
177
+ const dynamicRequireVars = new Set();
178
+ // Track variables assigned dangerous command strings (curl, wget, etc.)
179
+ const dangerousCmdVars = new Map();
180
+ // Track variables assigned paths containing .github/workflows
181
+ const workflowPathVars = new Set();
182
+ // Track variables assigned temp/executable file paths
183
+ const execPathVars = new Map();
184
+
185
+ /**
186
+ * Extract string value from a node if it's a Literal or TemplateLiteral with no expressions.
187
+ */
188
+ function extractStringValue(node) {
189
+ if (!node) return null;
190
+ if (node.type === 'Literal' && typeof node.value === 'string') return node.value;
191
+ if (node.type === 'TemplateLiteral' && node.expressions.length === 0) {
192
+ return node.quasis.map(q => q.value.raw).join('');
193
+ }
194
+ // Template literal with expressions — concatenate what we can
195
+ if (node.type === 'TemplateLiteral') {
196
+ return node.quasis.map(q => q.value.raw).join('***');
197
+ }
198
+ return null;
199
+ }
200
+
114
201
  walk.simple(ast, {
202
+ VariableDeclarator(node) {
203
+ if (node.id?.type === 'Identifier') {
204
+ // Track dynamic require vars
205
+ if (node.init?.type === 'CallExpression') {
206
+ const initCallName = getCallName(node.init);
207
+ if (initCallName === 'require' && node.init.arguments.length > 0) {
208
+ const arg = node.init.arguments[0];
209
+ if (arg.type !== 'Literal') {
210
+ dynamicRequireVars.add(node.id.name);
211
+ }
212
+ }
213
+ }
214
+ // Track variables assigned dangerous command strings
215
+ const strVal = extractStringValue(node.init);
216
+ if (strVal && DANGEROUS_CMD_PATTERNS.some(p => p.test(strVal))) {
217
+ dangerousCmdVars.set(node.id.name, strVal);
218
+ }
219
+
220
+ // Track variables assigned temp/executable file paths
221
+ if (strVal && /^\/tmp\/|^\/var\/tmp\/|\\temp\\/i.test(strVal)) {
222
+ execPathVars.set(node.id.name, strVal);
223
+ }
224
+
225
+ // Track variables assigned from path.join containing .github/workflows
226
+ if (node.init?.type === 'CallExpression' && node.init.callee?.type === 'MemberExpression') {
227
+ const obj = node.init.callee.object;
228
+ const prop = node.init.callee.property;
229
+ if (obj?.type === 'Identifier' && obj.name === 'path' &&
230
+ prop?.type === 'Identifier' && (prop.name === 'join' || prop.name === 'resolve')) {
231
+ const joinArgs = node.init.arguments.map(a => extractStringValue(a) || '').join('/');
232
+ if (/\.github[\\/\/]workflows/i.test(joinArgs) || /\.github[\\/\/]actions/i.test(joinArgs)) {
233
+ workflowPathVars.add(node.id.name);
234
+ }
235
+ }
236
+ }
237
+ }
238
+ },
239
+
115
240
  CallExpression(node) {
116
241
  const callName = getCallName(node);
117
242
 
243
+ // Detect require() with non-literal argument (obfuscation)
244
+ if (callName === 'require' && node.arguments.length > 0) {
245
+ const arg = node.arguments[0];
246
+ if (arg.type === 'BinaryExpression' && arg.operator === '+') {
247
+ threats.push({
248
+ type: 'dynamic_require',
249
+ severity: 'HIGH',
250
+ message: 'Dynamic require() with string concatenation (module name obfuscation).',
251
+ file: path.relative(basePath, filePath)
252
+ });
253
+ } else if (arg.type === 'TemplateLiteral' && arg.expressions.length > 0) {
254
+ threats.push({
255
+ type: 'dynamic_require',
256
+ severity: 'HIGH',
257
+ message: 'Dynamic require() with template literal (module name obfuscation).',
258
+ file: path.relative(basePath, filePath)
259
+ });
260
+ } else if (arg.type === 'CallExpression') {
261
+ const argCallName = getCallName(arg);
262
+ // Skip safe patterns: require(path.join(...)), require(path.resolve(...))
263
+ if (argCallName !== 'path.join' && argCallName !== 'path.resolve') {
264
+ const hasDecode = containsDecodePattern(arg);
265
+ threats.push({
266
+ type: 'dynamic_require',
267
+ severity: hasDecode ? 'CRITICAL' : 'HIGH',
268
+ message: hasDecode
269
+ ? 'Dynamic require() with runtime decode (base64/atob obfuscation).'
270
+ : 'Dynamic require() with computed argument (possible decode obfuscation).',
271
+ file: path.relative(basePath, filePath)
272
+ });
273
+ }
274
+ } else if (arg.type === 'Identifier') {
275
+ threats.push({
276
+ type: 'dynamic_require',
277
+ severity: 'HIGH',
278
+ message: 'Dynamic require() with variable argument (module name obfuscation).',
279
+ file: path.relative(basePath, filePath)
280
+ });
281
+ }
282
+ }
283
+
284
+ // Detect exec/execSync with dangerous shell commands (direct or via MemberExpression)
285
+ const execName = callName === 'exec' || callName === 'execSync' ? callName : null;
286
+ const memberExec = !execName && node.callee.type === 'MemberExpression' &&
287
+ node.callee.property?.type === 'Identifier' &&
288
+ (node.callee.property.name === 'exec' || node.callee.property.name === 'execSync')
289
+ ? node.callee.property.name : null;
290
+ if ((execName || memberExec) && node.arguments.length > 0) {
291
+ const arg = node.arguments[0];
292
+ let cmdStr = null;
293
+ if (arg.type === 'Literal' && typeof arg.value === 'string') {
294
+ cmdStr = arg.value;
295
+ } else if (arg.type === 'Identifier' && dangerousCmdVars.has(arg.name)) {
296
+ // Variable was assigned a dangerous command string
297
+ cmdStr = dangerousCmdVars.get(arg.name);
298
+ } else if (arg.type === 'Identifier' && execPathVars.has(arg.name)) {
299
+ // Variable was assigned a temp/executable file path
300
+ cmdStr = execPathVars.get(arg.name);
301
+ } else if (arg.type === 'TemplateLiteral') {
302
+ cmdStr = arg.quasis.map(q => q.value.raw).join('***');
303
+ }
304
+
305
+ if (cmdStr) {
306
+ // Check for dangerous shell patterns
307
+ 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)) {
308
+ threats.push({
309
+ type: 'dangerous_exec',
310
+ severity: 'CRITICAL',
311
+ message: `Dangerous shell command in exec(): "${cmdStr.substring(0, 80)}"`,
312
+ file: path.relative(basePath, filePath)
313
+ });
314
+ }
315
+
316
+ // Check for temp file execution (binary dropper pattern)
317
+ if (/^\/tmp\/|^\/var\/tmp\//i.test(cmdStr)) {
318
+ threats.push({
319
+ type: 'dangerous_exec',
320
+ severity: 'CRITICAL',
321
+ message: `Execution of temp file "${cmdStr.substring(0, 80)}" — binary dropper pattern.`,
322
+ file: path.relative(basePath, filePath)
323
+ });
324
+ }
325
+
326
+ // Check for credential-stealing CLI commands
327
+ for (const credCmd of CREDENTIAL_CLI_COMMANDS) {
328
+ if (cmdStr.includes(credCmd)) {
329
+ threats.push({
330
+ type: 'credential_command_exec',
331
+ severity: 'CRITICAL',
332
+ message: `Credential theft via CLI tool: exec("${credCmd}") — steals auth tokens from installed tools.`,
333
+ file: path.relative(basePath, filePath)
334
+ });
335
+ break;
336
+ }
337
+ }
338
+ }
339
+ }
340
+
341
+ // Detect exec/execSync called on a dynamically-required module variable
342
+ // Pattern: const mod = require(obfuscated); mod.exec(...) → obfuscated command execution
343
+ // Note: getCallName returns 'exec' for both exec() and mod.exec(), so we check execName too
344
+ if ((execName || memberExec) && node.callee.type === 'MemberExpression' && node.callee.object?.type === 'Identifier') {
345
+ if (dynamicRequireVars.has(node.callee.object.name)) {
346
+ const method = execName || memberExec;
347
+ threats.push({
348
+ type: 'dynamic_require_exec',
349
+ severity: 'CRITICAL',
350
+ message: `${method}() called on dynamically-required module "${node.callee.object.name}" — obfuscated command execution.`,
351
+ file: path.relative(basePath, filePath)
352
+ });
353
+ }
354
+ }
355
+
356
+ // Detect sandbox/container evasion: fs.accessSync('/.dockerenv'), fs.existsSync('/.dockerenv'), etc.
357
+ if (node.callee.type === 'MemberExpression' && node.callee.property?.type === 'Identifier') {
358
+ const fsMethod = node.callee.property.name;
359
+ if (['accessSync', 'existsSync', 'statSync', 'lstatSync', 'access', 'stat'].includes(fsMethod)) {
360
+ const arg = node.arguments[0];
361
+ if (arg?.type === 'Literal' && typeof arg.value === 'string') {
362
+ if (SANDBOX_INDICATORS.some(ind => arg.value.includes(ind))) {
363
+ threats.push({
364
+ type: 'sandbox_evasion',
365
+ severity: 'HIGH',
366
+ message: `Sandbox/container detection via ${fsMethod}("${arg.value}") — anti-analysis technique.`,
367
+ file: path.relative(basePath, filePath)
368
+ });
369
+ }
370
+ }
371
+ }
372
+ }
373
+
374
+ // Detect spawn/fork with {detached: true} — background process evasion
375
+ if ((callName === 'spawn' || callName === 'fork') && node.arguments.length >= 2) {
376
+ const lastArg = node.arguments[node.arguments.length - 1];
377
+ if (lastArg.type === 'ObjectExpression') {
378
+ const hasDetached = lastArg.properties.some(p =>
379
+ p.key?.type === 'Identifier' && p.key.name === 'detached' &&
380
+ p.value?.type === 'Literal' && p.value.value === true
381
+ );
382
+ if (hasDetached) {
383
+ threats.push({
384
+ type: 'detached_process',
385
+ severity: 'HIGH',
386
+ message: `${callName}() with {detached: true} — background process survives parent exit (evasion technique).`,
387
+ file: path.relative(basePath, filePath)
388
+ });
389
+ }
390
+ }
391
+ }
392
+
393
+ // Detect fs.writeFileSync/writeFile to .github/workflows — workflow injection persistence
394
+ if (node.callee.type === 'MemberExpression' && node.callee.property?.type === 'Identifier') {
395
+ const writeMethod = node.callee.property.name;
396
+ if (['writeFileSync', 'writeFile'].includes(writeMethod) && node.arguments.length > 0) {
397
+ const pathArg = node.arguments[0];
398
+ const pathStr = extractStringValue(pathArg);
399
+ // Also check path.join() arguments
400
+ let joinedPath = null;
401
+ let hasWorkflowVar = false;
402
+ if (pathArg?.type === 'CallExpression' && pathArg.arguments) {
403
+ joinedPath = pathArg.arguments.map(a => {
404
+ if (a.type === 'Identifier' && workflowPathVars.has(a.name)) {
405
+ hasWorkflowVar = true;
406
+ return '.github/workflows';
407
+ }
408
+ return extractStringValue(a) || '';
409
+ }).join('/');
410
+ }
411
+ // Direct Identifier reference to a tracked workflow path variable
412
+ if (pathArg?.type === 'Identifier' && workflowPathVars.has(pathArg.name)) {
413
+ hasWorkflowVar = true;
414
+ }
415
+ const checkPath = pathStr || joinedPath || '';
416
+ if (hasWorkflowVar || /\.github[\\/]workflows/i.test(checkPath) || /\.github[\\/]actions/i.test(checkPath)) {
417
+ threats.push({
418
+ type: 'workflow_write',
419
+ severity: 'HIGH',
420
+ message: `${writeMethod}() creates file in .github/workflows — GitHub Actions persistence technique.`,
421
+ file: path.relative(basePath, filePath)
422
+ });
423
+ }
424
+ }
425
+ }
426
+
427
+ // Detect fs.mkdirSync creating .github/workflows — part of workflow injection
428
+ if (node.callee.type === 'MemberExpression' && node.callee.property?.type === 'Identifier') {
429
+ const mkdirMethod = node.callee.property.name;
430
+ if ((mkdirMethod === 'mkdirSync' || mkdirMethod === 'mkdir') && node.arguments.length > 0) {
431
+ const pathArg = node.arguments[0];
432
+ // Check if it's a tracked workflow path variable
433
+ if (pathArg?.type === 'Identifier' && workflowPathVars.has(pathArg.name)) {
434
+ threats.push({
435
+ type: 'workflow_write',
436
+ severity: 'HIGH',
437
+ message: `${mkdirMethod}() creates .github/workflows directory — GitHub Actions persistence technique.`,
438
+ file: path.relative(basePath, filePath)
439
+ });
440
+ }
441
+ }
442
+ }
443
+
444
+ // Detect fs.chmodSync with executable permissions — binary dropper pattern
445
+ if (node.callee.type === 'MemberExpression' && node.callee.property?.type === 'Identifier') {
446
+ const chmodMethod = node.callee.property.name;
447
+ if ((chmodMethod === 'chmodSync' || chmodMethod === 'chmod') && node.arguments.length >= 2) {
448
+ const modeArg = node.arguments[1];
449
+ if (modeArg?.type === 'Literal' && typeof modeArg.value === 'number') {
450
+ // 0o755=493, 0o777=511, 0o700=448, 0o775=509
451
+ if (modeArg.value === 493 || modeArg.value === 511 || modeArg.value === 448 || modeArg.value === 509) {
452
+ threats.push({
453
+ type: 'binary_dropper',
454
+ severity: 'CRITICAL',
455
+ message: `${chmodMethod}() with executable permissions (0o${modeArg.value.toString(8)}) — binary dropper pattern.`,
456
+ file: path.relative(basePath, filePath)
457
+ });
458
+ }
459
+ }
460
+ }
461
+ }
462
+
463
+ // Detect AI agent weaponization: spawn/exec of AI agents with dangerous flags
464
+ // s1ngularity/Nx pattern (Aug 2025): invoke local AI coding agents to steal credentials
465
+ if ((callName === 'spawn' || callName === 'exec' || callName === 'execSync' ||
466
+ callName === 'execFile' || callName === 'execFileSync' || memberExec) &&
467
+ node.arguments.length > 0) {
468
+ // Collect all string literals in arguments (including arrays)
469
+ const argStrings = [];
470
+ for (const arg of node.arguments) {
471
+ if (arg.type === 'Literal' && typeof arg.value === 'string') {
472
+ argStrings.push(arg.value);
473
+ } else if (arg.type === 'ArrayExpression') {
474
+ for (const el of arg.elements) {
475
+ if (el && el.type === 'Literal' && typeof el.value === 'string') {
476
+ argStrings.push(el.value);
477
+ }
478
+ }
479
+ }
480
+ }
481
+
482
+ const allArgText = argStrings.join(' ');
483
+ const hasDangerousFlag = AI_AGENT_DANGEROUS_FLAGS.some(flag => allArgText.includes(flag));
484
+ const firstArg = node.arguments[0];
485
+ const cmdName = firstArg?.type === 'Literal' && typeof firstArg.value === 'string'
486
+ ? firstArg.value.toLowerCase() : '';
487
+ const isAIAgent = AI_AGENT_BINARIES.some(bin => cmdName === bin || cmdName.endsWith('/' + bin));
488
+
489
+ if (hasDangerousFlag) {
490
+ const matchedFlag = AI_AGENT_DANGEROUS_FLAGS.find(flag => allArgText.includes(flag));
491
+ threats.push({
492
+ type: 'ai_agent_abuse',
493
+ severity: 'CRITICAL',
494
+ message: `AI agent invoked with security bypass flag "${matchedFlag}"${isAIAgent ? ` (agent: ${cmdName})` : ''} — weaponized AI coding assistant (s1ngularity/Nx pattern).`,
495
+ file: path.relative(basePath, filePath)
496
+ });
497
+ } else if (isAIAgent) {
498
+ // AI agent binary called without known dangerous flag — still suspicious in a package
499
+ threats.push({
500
+ type: 'ai_agent_abuse',
501
+ severity: 'HIGH',
502
+ message: `AI coding agent "${cmdName}" invoked from package — potential AI agent weaponization.`,
503
+ file: path.relative(basePath, filePath)
504
+ });
505
+ }
506
+ }
507
+
118
508
  if (callName === 'eval') {
119
509
  const isConstant = hasOnlyStringLiteralArgs(node);
120
510
  threats.push({
@@ -140,6 +530,33 @@ function analyzeFile(content, filePath, basePath) {
140
530
  }
141
531
  },
142
532
 
533
+ ImportExpression(node) {
534
+ // import() dynamic — same concern as dynamic require()
535
+ if (node.source) {
536
+ const src = node.source;
537
+ if (src.type === 'Literal' && typeof src.value === 'string') {
538
+ // Static import('fs') — flag dangerous modules
539
+ const dangerousModules = ['child_process', 'fs', 'http', 'https', 'net', 'dns', 'tls'];
540
+ if (dangerousModules.includes(src.value)) {
541
+ threats.push({
542
+ type: 'dynamic_import',
543
+ severity: 'HIGH',
544
+ message: `Dynamic import() of dangerous module "${src.value}".`,
545
+ file: path.relative(basePath, filePath)
546
+ });
547
+ }
548
+ } else {
549
+ // Non-literal import source — obfuscation
550
+ threats.push({
551
+ type: 'dynamic_import',
552
+ severity: 'HIGH',
553
+ message: 'Dynamic import() with computed argument (possible obfuscation).',
554
+ file: path.relative(basePath, filePath)
555
+ });
556
+ }
557
+ }
558
+ },
559
+
143
560
  NewExpression(node) {
144
561
  if (node.callee.type === 'Identifier' && node.callee.name === 'Function') {
145
562
  const isConstant = hasOnlyStringLiteralArgs(node);
@@ -152,6 +569,21 @@ function analyzeFile(content, filePath, basePath) {
152
569
  file: path.relative(basePath, filePath)
153
570
  });
154
571
  }
572
+
573
+ // Detect new Proxy(process.env, handler) — env interception
574
+ if (node.callee.type === 'Identifier' && node.callee.name === 'Proxy' && node.arguments.length >= 2) {
575
+ const target = node.arguments[0];
576
+ if (target.type === 'MemberExpression' &&
577
+ target.object?.name === 'process' &&
578
+ target.property?.name === 'env') {
579
+ threats.push({
580
+ type: 'env_proxy_intercept',
581
+ severity: 'CRITICAL',
582
+ message: 'new Proxy(process.env) detected — intercepts all environment variable access.',
583
+ file: path.relative(basePath, filePath)
584
+ });
585
+ }
586
+ }
155
587
  },
156
588
 
157
589
  Literal(node) {
@@ -160,7 +592,7 @@ function analyzeFile(content, filePath, basePath) {
160
592
  if (SAFE_STRINGS.some(s => node.value.includes(s))) {
161
593
  return;
162
594
  }
163
-
595
+
164
596
  for (const sensitive of SENSITIVE_STRINGS) {
165
597
  if (node.value.includes(sensitive)) {
166
598
  threats.push({
@@ -171,6 +603,68 @@ function analyzeFile(content, filePath, basePath) {
171
603
  });
172
604
  }
173
605
  }
606
+
607
+ // Detect AI agent dangerous flags as string literals anywhere in code
608
+ for (const flag of AI_AGENT_DANGEROUS_FLAGS) {
609
+ if (node.value === flag) {
610
+ threats.push({
611
+ type: 'ai_agent_abuse',
612
+ severity: 'CRITICAL',
613
+ message: `AI agent security bypass flag "${flag}" found — weaponized AI coding assistant (s1ngularity/Nx pattern).`,
614
+ file: path.relative(basePath, filePath)
615
+ });
616
+ }
617
+ }
618
+ }
619
+ },
620
+
621
+ AssignmentExpression(node) {
622
+ // Detect prototype hooking of native browser/network APIs
623
+ // Pattern: globalThis.fetch = function(...) or XMLHttpRequest.prototype.send = function(...)
624
+ if (node.left?.type === 'MemberExpression') {
625
+ const left = node.left;
626
+
627
+ // globalThis.fetch = ... or globalThis.XMLHttpRequest = ...
628
+ if (left.object?.type === 'Identifier' && left.object.name === 'globalThis' &&
629
+ left.property?.type === 'Identifier') {
630
+ if (HOOKABLE_NATIVES.includes(left.property.name)) {
631
+ threats.push({
632
+ type: 'prototype_hook',
633
+ severity: 'HIGH',
634
+ message: `globalThis.${left.property.name} overridden — native API hooking for traffic interception.`,
635
+ file: path.relative(basePath, filePath)
636
+ });
637
+ }
638
+ }
639
+
640
+ // XMLHttpRequest.prototype.send = ... or Response.prototype.json = ...
641
+ if (left.object?.type === 'MemberExpression' &&
642
+ left.object.property?.type === 'Identifier' &&
643
+ left.object.property.name === 'prototype' &&
644
+ left.object.object?.type === 'Identifier') {
645
+ if (HOOKABLE_NATIVES.includes(left.object.object.name)) {
646
+ threats.push({
647
+ type: 'prototype_hook',
648
+ severity: 'HIGH',
649
+ message: `${left.object.object.name}.prototype.${left.property?.name || '?'} overridden — native API hooking for traffic interception.`,
650
+ file: path.relative(basePath, filePath)
651
+ });
652
+ }
653
+ }
654
+
655
+ // http.request = ... or https.get = ... (Node.js module hooking)
656
+ if (left.object?.type === 'Identifier' &&
657
+ ['http', 'https'].includes(left.object.name) &&
658
+ left.property?.type === 'Identifier' &&
659
+ ['request', 'get', 'createServer'].includes(left.property.name) &&
660
+ node.right?.type === 'FunctionExpression') {
661
+ threats.push({
662
+ type: 'prototype_hook',
663
+ severity: 'HIGH',
664
+ message: `${left.object.name}.${left.property.name} overridden — Node.js network module hooking for traffic interception.`,
665
+ file: path.relative(basePath, filePath)
666
+ });
667
+ }
174
668
  }
175
669
  },
176
670