muaddib-scanner 2.5.3 → 2.5.4

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "muaddib-scanner",
3
- "version": "2.5.3",
3
+ "version": "2.5.4",
4
4
  "description": "Supply-chain threat detection & response for npm & PyPI/Python",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -2,7 +2,7 @@
2
2
  * MUAD'DIB Sandbox Preload Log Analyzer
3
3
  *
4
4
  * Parses [PRELOAD] log lines produced by docker/preload.js and generates
5
- * scored findings for behavioral analysis. Six detection rules:
5
+ * scored findings for behavioral analysis. Seven detection rules:
6
6
  *
7
7
  * 1. sandbox_timer_delay_suspicious — timer delay > 1h (MEDIUM, +15)
8
8
  * 2. sandbox_timer_delay_critical — timer delay > 24h (CRITICAL, +30, supersedes #1)
@@ -10,6 +10,7 @@
10
10
  * 4. sandbox_network_after_sensitive_read — network call after sensitive read (CRITICAL, +40)
11
11
  * 5. sandbox_exec_suspicious — dangerous command execution (HIGH, +25)
12
12
  * 6. sandbox_env_token_access — sensitive env var access (MEDIUM, +10)
13
+ * 7. sandbox_native_addon_load — native .node addon loaded (MEDIUM, +15)
13
14
  */
14
15
 
15
16
  const ONE_HOUR_MS = 3600000;
@@ -38,6 +39,7 @@ function analyzePreloadLog(logContent) {
38
39
  const networkLines = [];
39
40
  const execLines = [];
40
41
  const envLines = [];
42
+ const nativeAddonLines = [];
41
43
 
42
44
  for (const line of lines) {
43
45
  if (line.includes('TIMER:')) {
@@ -52,6 +54,8 @@ function analyzePreloadLog(logContent) {
52
54
  execLines.push(line);
53
55
  } else if (line.includes('ENV_ACCESS:')) {
54
56
  envLines.push(line);
57
+ } else if (line.includes('NATIVE_ADDON:')) {
58
+ nativeAddonLines.push(line);
55
59
  }
56
60
  }
57
61
 
@@ -173,6 +177,24 @@ function analyzePreloadLog(logContent) {
173
177
  });
174
178
  }
175
179
 
180
+ // ── Rule 7: Native addon loading ──
181
+ // Native addons (.node files) can bypass all JS monkey-patches via syscalls.
182
+ // Flag their loading so analysts know time-based evasion may be undetected.
183
+ if (nativeAddonLines.length > 0) {
184
+ const addons = nativeAddonLines.map(l => {
185
+ const m = l.match(/process\.dlopen:\s*(.+?)(?:\s+\(t\+|$)/);
186
+ return m ? m[1].trim() : 'unknown';
187
+ });
188
+
189
+ score += 15;
190
+ findings.push({
191
+ type: 'sandbox_native_addon_load',
192
+ severity: 'MEDIUM',
193
+ detail: `Native addon loaded (${addons.length}): time-based evasion via syscalls possible`,
194
+ evidence: addons.join(', ')
195
+ });
196
+ }
197
+
176
198
  return {
177
199
  score: Math.min(100, score),
178
200
  findings
@@ -862,6 +862,81 @@ function handleCallExpression(node, ctx) {
862
862
  }
863
863
  }
864
864
 
865
+ // Detect eval.call(null, code) / eval.apply(null, [code]) / Function.call/apply
866
+ if (node.callee.type === 'MemberExpression' && !node.callee.computed &&
867
+ node.callee.property?.type === 'Identifier' &&
868
+ (node.callee.property.name === 'call' || node.callee.property.name === 'apply')) {
869
+ const obj = node.callee.object;
870
+ if (obj?.type === 'Identifier' && (obj.name === 'eval' || obj.name === 'Function')) {
871
+ ctx.hasEvalInFile = true;
872
+ ctx.hasDynamicExec = true;
873
+ ctx.threats.push({
874
+ type: obj.name === 'eval' ? 'dangerous_call_eval' : 'dangerous_call_function',
875
+ severity: 'HIGH',
876
+ message: `${obj.name}.${node.callee.property.name}() — indirect execution via call/apply evasion technique.`,
877
+ file: ctx.relFile
878
+ });
879
+ }
880
+ }
881
+
882
+ // Detect array access pattern: [require][0]('child_process') or [eval][0](code)
883
+ if (node.callee.type === 'MemberExpression' && node.callee.computed &&
884
+ node.callee.object?.type === 'ArrayExpression' &&
885
+ node.callee.property?.type === 'Literal' && typeof node.callee.property.value === 'number') {
886
+ const elements = node.callee.object.elements;
887
+ for (const el of elements) {
888
+ if (el?.type === 'Identifier') {
889
+ if (el.name === 'eval') {
890
+ ctx.hasEvalInFile = true;
891
+ ctx.hasDynamicExec = true;
892
+ ctx.threats.push({
893
+ type: 'dangerous_call_eval',
894
+ severity: 'HIGH',
895
+ message: '[eval][0]() — array access evasion technique for indirect eval execution.',
896
+ file: ctx.relFile
897
+ });
898
+ } else if (el.name === 'require') {
899
+ ctx.threats.push({
900
+ type: 'dynamic_require',
901
+ severity: 'HIGH',
902
+ message: '[require][0]() — array access evasion technique for indirect require.',
903
+ file: ctx.relFile
904
+ });
905
+ } else if (el.name === 'Function') {
906
+ ctx.hasDynamicExec = true;
907
+ ctx.threats.push({
908
+ type: 'dangerous_call_function',
909
+ severity: 'MEDIUM',
910
+ message: '[Function][0]() — array access evasion technique for indirect Function construction.',
911
+ file: ctx.relFile
912
+ });
913
+ }
914
+ }
915
+ }
916
+ }
917
+
918
+ // Detect new Proxy(require, handler) — proxy wrapping require to intercept module loading
919
+ if (node.callee.type === 'Identifier' && node.callee.name !== 'Proxy') {
920
+ // handled below in handleNewExpression
921
+ }
922
+
923
+ // Detect template literals in exec/execSync: execSync(`${cmd}`)
924
+ if ((execName || memberExec) && node.arguments.length > 0) {
925
+ const arg = node.arguments[0];
926
+ if (arg.type === 'TemplateLiteral' && arg.expressions.length > 0) {
927
+ // Template literal with dynamic expressions in exec — bypass for string matching
928
+ const staticParts = arg.quasis.map(q => q.value.raw).join('');
929
+ if (DANGEROUS_CMD_PATTERNS.some(p => p.test(staticParts))) {
930
+ ctx.threats.push({
931
+ type: 'dangerous_exec',
932
+ severity: 'CRITICAL',
933
+ message: `Dangerous command in template literal exec(): "${staticParts.substring(0, 80)}" — template literal evasion.`,
934
+ file: ctx.relFile
935
+ });
936
+ }
937
+ }
938
+ }
939
+
865
940
  // Detect indirect eval/Function via computed property
866
941
  if (node.callee.type === 'MemberExpression' && node.callee.computed) {
867
942
  const prop = node.callee.property;
@@ -1058,6 +1133,15 @@ function handleNewExpression(node, ctx) {
1058
1133
  file: ctx.relFile
1059
1134
  });
1060
1135
  }
1136
+ // Detect new Proxy(require, handler) — intercept module loading
1137
+ if (target.type === 'Identifier' && target.name === 'require') {
1138
+ ctx.threats.push({
1139
+ type: 'dynamic_require',
1140
+ severity: 'HIGH',
1141
+ message: 'new Proxy(require) — proxy wrapping require to intercept/redirect module loading.',
1142
+ file: ctx.relFile
1143
+ });
1144
+ }
1061
1145
  }
1062
1146
  }
1063
1147
 
@@ -1119,6 +1203,54 @@ function handleLiteral(node, ctx) {
1119
1203
  }
1120
1204
 
1121
1205
  function handleAssignmentExpression(node, ctx) {
1206
+ // Detect object property indirection: obj.exec = require('child_process').exec
1207
+ // or obj.fn = eval — stashing dangerous functions in object properties
1208
+ if (node.left?.type === 'MemberExpression' && node.right) {
1209
+ const propName = node.left.property?.type === 'Identifier' ? node.left.property.name :
1210
+ (node.left.property?.type === 'Literal' ? String(node.left.property.value) : null);
1211
+
1212
+ if (propName) {
1213
+ // Assigning require('child_process') or its methods to an object property
1214
+ if (node.right.type === 'CallExpression' && getCallName(node.right) === 'require' &&
1215
+ node.right.arguments.length > 0 && node.right.arguments[0]?.type === 'Literal') {
1216
+ const mod = node.right.arguments[0].value;
1217
+ if (mod === 'child_process' || mod === 'fs' || mod === 'net' || mod === 'dns') {
1218
+ ctx.threats.push({
1219
+ type: 'dynamic_require',
1220
+ severity: 'HIGH',
1221
+ message: `Object property indirection: ${propName} = require('${mod}') — hiding dangerous module in object property.`,
1222
+ file: ctx.relFile
1223
+ });
1224
+ }
1225
+ }
1226
+ // Assigning require('child_process').exec to an object property
1227
+ if (node.right.type === 'MemberExpression' && node.right.object?.type === 'CallExpression' &&
1228
+ getCallName(node.right.object) === 'require' &&
1229
+ node.right.object.arguments.length > 0 && node.right.object.arguments[0]?.type === 'Literal' &&
1230
+ node.right.object.arguments[0].value === 'child_process') {
1231
+ const method = node.right.property?.type === 'Identifier' ? node.right.property.name : null;
1232
+ if (method && ['exec', 'execSync', 'spawn', 'execFile'].includes(method)) {
1233
+ ctx.threats.push({
1234
+ type: 'dangerous_exec',
1235
+ severity: 'HIGH',
1236
+ message: `Object property indirection: ${propName} = require('child_process').${method} — hiding exec in object property.`,
1237
+ file: ctx.relFile
1238
+ });
1239
+ }
1240
+ }
1241
+ // Assigning eval or Function to an object property
1242
+ if (node.right.type === 'Identifier' && (node.right.name === 'eval' || node.right.name === 'Function')) {
1243
+ ctx.hasDynamicExec = true;
1244
+ ctx.threats.push({
1245
+ type: node.right.name === 'eval' ? 'dangerous_call_eval' : 'dangerous_call_function',
1246
+ severity: 'HIGH',
1247
+ message: `Object property indirection: ${propName} = ${node.right.name} — stashing dangerous function in object property.`,
1248
+ file: ctx.relFile
1249
+ });
1250
+ }
1251
+ }
1252
+ }
1253
+
1122
1254
  if (node.left?.type === 'MemberExpression') {
1123
1255
  const left = node.left;
1124
1256
 
@@ -1375,6 +1507,33 @@ function handlePostWalk(ctx) {
1375
1507
  }
1376
1508
  }
1377
1509
 
1510
+ function handleWithStatement(node, ctx) {
1511
+ // with(require('child_process')) exec(cmd) — scope injection evasion
1512
+ // The with() statement makes all properties of the object available as local variables.
1513
+ // When used with require(), it allows calling exec(), spawn() etc. without explicit reference.
1514
+ if (node.object?.type === 'CallExpression' && getCallName(node.object) === 'require') {
1515
+ const arg = node.object.arguments[0];
1516
+ const modName = arg?.type === 'Literal' ? arg.value : null;
1517
+ const dangerousModules = ['child_process', 'fs', 'http', 'https', 'net', 'dns'];
1518
+ if (modName && dangerousModules.includes(modName)) {
1519
+ ctx.hasDynamicExec = true;
1520
+ ctx.threats.push({
1521
+ type: 'dangerous_exec',
1522
+ severity: 'CRITICAL',
1523
+ message: `with(require('${modName}')) — scope injection evasion: all module methods available as local variables.`,
1524
+ file: ctx.relFile
1525
+ });
1526
+ } else if (!modName) {
1527
+ ctx.threats.push({
1528
+ type: 'dynamic_require',
1529
+ severity: 'HIGH',
1530
+ message: 'with(require(...)) — scope injection with dynamic module. Evasion technique.',
1531
+ file: ctx.relFile
1532
+ });
1533
+ }
1534
+ }
1535
+ }
1536
+
1378
1537
  module.exports = {
1379
1538
  handleVariableDeclarator,
1380
1539
  handleCallExpression,
@@ -1383,5 +1542,6 @@ module.exports = {
1383
1542
  handleLiteral,
1384
1543
  handleAssignmentExpression,
1385
1544
  handleMemberExpression,
1545
+ handleWithStatement,
1386
1546
  handlePostWalk
1387
1547
  };
@@ -11,6 +11,7 @@ const {
11
11
  handleLiteral,
12
12
  handleAssignmentExpression,
13
13
  handleMemberExpression,
14
+ handleWithStatement,
14
15
  handlePostWalk
15
16
  } = require('./ast-detectors.js');
16
17
 
@@ -121,7 +122,8 @@ function analyzeFile(content, filePath, basePath) {
121
122
  NewExpression(node) { handleNewExpression(node, ctx); },
122
123
  Literal(node) { handleLiteral(node, ctx); },
123
124
  AssignmentExpression(node) { handleAssignmentExpression(node, ctx); },
124
- MemberExpression(node) { handleMemberExpression(node, ctx); }
125
+ MemberExpression(node) { handleMemberExpression(node, ctx); },
126
+ WithStatement(node) { handleWithStatement(node, ctx); }
125
127
  });
126
128
 
127
129
  // FIX 5: DNS chunk exfiltration — verify dns.resolve* is inside a loop body