muaddib-scanner 1.4.2 → 1.5.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.
package/bin/muaddib.js CHANGED
@@ -242,6 +242,7 @@ const helpText = `
242
242
  muaddib update Update IOCs
243
243
  muaddib scrape Scrape new IOCs
244
244
  muaddib sandbox <pkg> Analyze in isolated Docker container
245
+ muaddib version Show version
245
246
 
246
247
  Diff Examples:
247
248
  muaddib diff HEAD~1 Compare with previous commit
@@ -267,7 +268,11 @@ const helpText = `
267
268
  `;
268
269
 
269
270
  // Main
270
- if (!command || command === '--help' || command === '-h') {
271
+ if (command === 'version' || command === '--version' || command === '-v') {
272
+ const pkg = require('../package.json');
273
+ console.log(`muaddib-scanner v${pkg.version}`);
274
+ process.exit(0);
275
+ } else if (!command || command === '--help' || command === '-h') {
271
276
  if (command === '--help' || command === '-h') {
272
277
  console.log(helpText);
273
278
  process.exit(0);
package/docker/Dockerfile CHANGED
@@ -1,7 +1,7 @@
1
1
  FROM node:20-alpine
2
2
 
3
3
  # Outils de monitoring
4
- RUN apk add --no-cache strace curl tcpdump
4
+ RUN apk add --no-cache strace curl tcpdump coreutils findutils jq
5
5
 
6
6
  # User non-root
7
7
  RUN adduser -D sandboxuser
@@ -1,26 +1,154 @@
1
1
  #!/bin/sh
2
- PACKAGE=$1
2
+ PACKAGE="$1"
3
3
 
4
- echo "[SANDBOX] Installing $PACKAGE..."
4
+ if [ -z "$PACKAGE" ]; then
5
+ echo "Usage: sandbox-runner.sh <package-name>" >&2
6
+ exit 1
7
+ fi
5
8
 
6
- # Capturer les connexions réseau en background
7
- tcpdump -i any -w /tmp/network.pcap 2>/dev/null &
9
+ TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
10
+ START_MS=$(date +%s%3N 2>/dev/null || echo 0)
11
+
12
+ # ── 1. Filesystem snapshot BEFORE install ──
13
+ echo "[SANDBOX] Snapshot filesystem before install..." >&2
14
+ find / -type f 2>/dev/null | sort > /tmp/fs-before.txt
15
+
16
+ # ── 2. tcpdump in background (DNS + HTTP + HTTPS) ──
17
+ echo "[SANDBOX] Starting network capture..." >&2
18
+ tcpdump -i any -nn 'port 53 or port 80 or port 443' -l > /tmp/network.log 2>/dev/null &
8
19
  TCPDUMP_PID=$!
20
+ sleep 1
21
+
22
+ # ── 3. npm install with strace ──
23
+ echo "[SANDBOX] Installing $PACKAGE..." >&2
24
+ strace -f -e trace=network,process,open,openat,connect,execve,sendto,recvfrom \
25
+ -o /tmp/strace.log \
26
+ npm install "$PACKAGE" --ignore-scripts=false > /tmp/install.log 2>&1
27
+ EXIT_CODE=$?
28
+
29
+ # ── 4. Filesystem snapshot AFTER install ──
30
+ echo "[SANDBOX] Snapshot filesystem after install..." >&2
31
+ find / -type f 2>/dev/null | sort > /tmp/fs-after.txt
32
+
33
+ # Stop tcpdump
34
+ kill "$TCPDUMP_PID" 2>/dev/null
35
+ wait "$TCPDUMP_PID" 2>/dev/null
36
+
37
+ END_MS=$(date +%s%3N 2>/dev/null || echo 0)
38
+ DURATION_MS=$((END_MS - START_MS))
39
+ [ "$DURATION_MS" -lt 0 ] 2>/dev/null && DURATION_MS=0
40
+
41
+ # ── 5. Filesystem diff (exclude /sandbox/node_modules/) ──
42
+ echo "[SANDBOX] Analyzing filesystem changes..." >&2
43
+ comm -13 /tmp/fs-before.txt /tmp/fs-after.txt | grep -v '^/sandbox/node_modules/' | grep -v '^/tmp/fs-\|^/tmp/install.log\|^/tmp/network.log\|^/tmp/strace.log\|^/tmp/sensitive-\|^/tmp/suspicious-cmds\|^/tmp/connections.txt\|^/tmp/dns-queries\|^/tmp/fs-created\|^/tmp/fs-deleted' > /tmp/fs-created.txt
44
+ comm -23 /tmp/fs-before.txt /tmp/fs-after.txt | grep -v '^/sandbox/node_modules/' > /tmp/fs-deleted.txt
45
+
46
+ # ── 6. Parse strace ──
47
+ echo "[SANDBOX] Parsing strace..." >&2
48
+
49
+ SENSITIVE='\.npmrc|\.ssh/|\.aws/|\.env|/etc/passwd|/etc/shadow|\.gitconfig|\.bash_history'
50
+
51
+ # 6a. Sensitive file access (read)
52
+ grep -E 'openat\(' /tmp/strace.log 2>/dev/null | \
53
+ grep -E "$SENSITIVE" | \
54
+ grep 'O_RDONLY' | \
55
+ sed 's/.*openat([^,]*, "\([^"]*\)".*/\1/' | \
56
+ sort -u > /tmp/sensitive-read.txt
57
+
58
+ # 6b. Sensitive file access (write)
59
+ grep -E 'openat\(' /tmp/strace.log 2>/dev/null | \
60
+ grep -E "$SENSITIVE" | \
61
+ grep -E 'O_WRONLY|O_RDWR|O_CREAT' | \
62
+ sed 's/.*openat([^,]*, "\([^"]*\)".*/\1/' | \
63
+ sort -u > /tmp/sensitive-written.txt
64
+
65
+ # 6c. Suspicious execve (exclude node, npm, npx, sh, git)
66
+ grep 'execve(' /tmp/strace.log 2>/dev/null | \
67
+ grep '= 0' | \
68
+ grep -vE 'execve\("[^"]*/(node|npm|npx|sh|git)"' | \
69
+ sed -n 's/.*\[pid \([0-9]*\)\].*execve("\([^"]*\)".*/\1\t\2/p' > /tmp/suspicious-cmds.txt
70
+
71
+ grep 'execve(' /tmp/strace.log 2>/dev/null | \
72
+ grep '= 0' | \
73
+ grep -vE 'execve\("[^"]*/(node|npm|npx|sh|git)"' | \
74
+ grep -v '\[pid' | \
75
+ sed -n 's/.*execve("\([^"]*\)".*/0\t\1/p' >> /tmp/suspicious-cmds.txt
76
+
77
+ # 6d. Outgoing connections (AF_INET, successful)
78
+ grep 'connect(' /tmp/strace.log 2>/dev/null | \
79
+ grep 'AF_INET' | grep -v 'AF_INET6' | \
80
+ grep '= 0' | \
81
+ sed -n 's/.*sin_port=htons(\([0-9]*\)).*sin_addr=inet_addr("\([^"]*\)").*/\2\t\1/p' | \
82
+ grep -v ' 65535$' | \
83
+ grep -v '^127\.' | \
84
+ sort -u > /tmp/connections.txt
85
+
86
+ # ── 7. Parse tcpdump ──
87
+ echo "[SANDBOX] Parsing network capture..." >&2
88
+
89
+ grep -oE '(A|AAAA)\? [^ ]+' /tmp/network.log 2>/dev/null | \
90
+ awk '{print $2}' | \
91
+ sed 's/\.$//' | \
92
+ sort -u > /tmp/dns-queries.txt
93
+
94
+ # ── 8. Build JSON with jq ──
95
+ echo "[SANDBOX] Building report..." >&2
9
96
 
10
- # Installer le package avec strace pour capturer les appels système
11
- strace -f -e trace=network,process,file -o /tmp/strace.log npm install "$PACKAGE" --ignore-scripts=false 2>&1
97
+ # Ensure all temp files exist
98
+ touch /tmp/fs-created.txt /tmp/fs-deleted.txt /tmp/dns-queries.txt \
99
+ /tmp/sensitive-read.txt /tmp/sensitive-written.txt \
100
+ /tmp/connections.txt /tmp/suspicious-cmds.txt /tmp/install.log
12
101
 
13
- # Arrêter tcpdump
14
- kill $TCPDUMP_PID 2>/dev/null
102
+ INSTALL_OUTPUT=$(head -c 5000 /tmp/install.log)
15
103
 
16
- # Analyser les résultats
17
- echo "[SANDBOX] === NETWORK CONNECTIONS ==="
18
- grep -E "connect|sendto" /tmp/strace.log | head -20
104
+ FS_CREATED=$(jq -R -s 'split("\n") | map(select(length > 0))' < /tmp/fs-created.txt)
105
+ FS_DELETED=$(jq -R -s 'split("\n") | map(select(length > 0))' < /tmp/fs-deleted.txt)
106
+ DNS=$(jq -R -s 'split("\n") | map(select(length > 0))' < /tmp/dns-queries.txt)
107
+ SENS_READ=$(jq -R -s 'split("\n") | map(select(length > 0))' < /tmp/sensitive-read.txt)
108
+ SENS_WRITTEN=$(jq -R -s 'split("\n") | map(select(length > 0))' < /tmp/sensitive-written.txt)
19
109
 
20
- echo "[SANDBOX] === PROCESS SPAWNS ==="
21
- grep -E "execve|clone" /tmp/strace.log | head -20
110
+ CONNS=$(jq -R -s 'split("\n") | map(select(length > 0)) | map(
111
+ split("\t") | {host: .[0], port: (.[1] | tonumber), protocol: "TCP"}
112
+ )' < /tmp/connections.txt)
22
113
 
23
- echo "[SANDBOX] === FILE ACCESS ==="
24
- grep -E "openat.*npmrc|openat.*ssh|openat.*aws" /tmp/strace.log | head -20
114
+ PROCS=$(jq -R -s 'split("\n") | map(select(length > 0)) | map(
115
+ split("\t") | {command: .[1], pid: (.[0] | tonumber)}
116
+ )' < /tmp/suspicious-cmds.txt)
25
117
 
26
- echo "[SANDBOX] Done."
118
+ # ── Final JSON (ONLY output on stdout) ──
119
+ jq -n \
120
+ --arg package "$PACKAGE" \
121
+ --arg timestamp "$TIMESTAMP" \
122
+ --argjson duration "${DURATION_MS:-0}" \
123
+ --argjson fs_created "$FS_CREATED" \
124
+ --argjson fs_deleted "$FS_DELETED" \
125
+ --argjson dns "$DNS" \
126
+ --argjson connections "$CONNS" \
127
+ --argjson processes "$PROCS" \
128
+ --argjson sensitive_read "$SENS_READ" \
129
+ --argjson sensitive_written "$SENS_WRITTEN" \
130
+ --arg install_output "$INSTALL_OUTPUT" \
131
+ --argjson exit_code "${EXIT_CODE:-1}" \
132
+ '{
133
+ package: $package,
134
+ timestamp: $timestamp,
135
+ duration_ms: $duration,
136
+ filesystem: {
137
+ created: $fs_created,
138
+ deleted: $fs_deleted,
139
+ modified: []
140
+ },
141
+ network: {
142
+ dns_queries: $dns,
143
+ http_connections: $connections
144
+ },
145
+ processes: {
146
+ spawned: $processes
147
+ },
148
+ sensitive_files: {
149
+ read: $sensitive_read,
150
+ written: $sensitive_written
151
+ },
152
+ install_output: $install_output,
153
+ exit_code: $exit_code
154
+ }'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "muaddib-scanner",
3
- "version": "1.4.2",
3
+ "version": "1.5.0",
4
4
  "description": "Supply-chain threat detection & response for npm",
5
5
  "main": "src/index.js",
6
6
  "bin": {
package/src/index.js CHANGED
@@ -150,8 +150,43 @@ async function run(targetPath, options = {}) {
150
150
  threats.push(...paranoidThreats);
151
151
  }
152
152
 
153
+ // Sandbox integration
154
+ let sandboxData = null;
155
+ if (options.sandboxResult && options.sandboxResult.findings) {
156
+ const sr = options.sandboxResult;
157
+ const pkg = sr.raw_report?.package || 'unknown';
158
+ sandboxData = {
159
+ package: pkg,
160
+ score: sr.score,
161
+ severity: sr.severity,
162
+ findings: sr.findings
163
+ };
164
+ for (const f of sr.findings) {
165
+ threats.push({
166
+ type: 'sandbox_' + f.type,
167
+ severity: f.severity,
168
+ message: f.detail,
169
+ file: `[SANDBOX] ${pkg}`
170
+ });
171
+ }
172
+ }
173
+
174
+ // Deduplicate: same file + same type + same message = show once with count
175
+ const deduped = [];
176
+ const seen = new Map();
177
+ for (const t of threats) {
178
+ const key = `${t.file}::${t.type}::${t.message}`;
179
+ if (seen.has(key)) {
180
+ seen.get(key).count++;
181
+ } else {
182
+ const entry = { ...t, count: 1 };
183
+ seen.set(key, entry);
184
+ deduped.push(entry);
185
+ }
186
+ }
187
+
153
188
  // Enrich each threat with rules
154
- const enrichedThreats = threats.map(t => {
189
+ const enrichedThreats = deduped.map(t => {
155
190
  const rule = getRule(t.type);
156
191
  return {
157
192
  ...t,
@@ -164,11 +199,11 @@ async function run(targetPath, options = {}) {
164
199
  };
165
200
  });
166
201
 
167
- // Calculate risk score (0-100)
168
- const criticalCount = threats.filter(t => t.severity === 'CRITICAL').length;
169
- const highCount = threats.filter(t => t.severity === 'HIGH').length;
170
- const mediumCount = threats.filter(t => t.severity === 'MEDIUM').length;
171
- const lowCount = threats.filter(t => t.severity === 'LOW').length;
202
+ // Calculate risk score (0-100) using deduplicated threats
203
+ const criticalCount = deduped.filter(t => t.severity === 'CRITICAL').length;
204
+ const highCount = deduped.filter(t => t.severity === 'HIGH').length;
205
+ const mediumCount = deduped.filter(t => t.severity === 'MEDIUM').length;
206
+ const lowCount = deduped.filter(t => t.severity === 'LOW').length;
172
207
 
173
208
  let riskScore = 0;
174
209
  riskScore += criticalCount * SEVERITY_WEIGHTS.CRITICAL;
@@ -188,14 +223,15 @@ async function run(targetPath, options = {}) {
188
223
  timestamp: new Date().toISOString(),
189
224
  threats: enrichedThreats,
190
225
  summary: {
191
- total: threats.length,
226
+ total: deduped.length,
192
227
  critical: criticalCount,
193
228
  high: highCount,
194
229
  medium: mediumCount,
195
230
  low: lowCount,
196
231
  riskScore: riskScore,
197
232
  riskLevel: riskLevel
198
- }
233
+ },
234
+ sandbox: sandboxData
199
235
  };
200
236
 
201
237
  // JSON output
@@ -222,7 +258,8 @@ async function run(targetPath, options = {}) {
222
258
  console.log(`[ALERT] ${enrichedThreats.length} threat(s) detected:\n`);
223
259
  enrichedThreats.forEach((t, i) => {
224
260
  console.log(`━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`);
225
- console.log(` ${i + 1}. [${t.severity}] ${t.rule_name}`);
261
+ const countStr = t.count > 1 ? ` (x${t.count})` : '';
262
+ console.log(` ${i + 1}. [${t.severity}] ${t.rule_name}${countStr}`);
226
263
  console.log(` Rule ID: ${t.rule_id}`);
227
264
  console.log(` File: ${t.file}`);
228
265
  if (t.line) console.log(` Line: ${t.line}`);
@@ -237,6 +274,22 @@ async function run(targetPath, options = {}) {
237
274
  console.log('');
238
275
  });
239
276
  }
277
+
278
+ // Sandbox section (explain)
279
+ if (sandboxData) {
280
+ console.log(`\n[SANDBOX] Dynamic analysis — ${sandboxData.package}`);
281
+ console.log(` Score: ${sandboxData.score}/100`);
282
+ console.log(` Severity: ${sandboxData.severity}`);
283
+ if (sandboxData.findings.length === 0) {
284
+ console.log(' No suspicious behavior detected.\n');
285
+ } else {
286
+ console.log(` ${sandboxData.findings.length} finding(s):`);
287
+ sandboxData.findings.forEach(f => {
288
+ console.log(` [${f.severity}] ${f.type}: ${f.detail}`);
289
+ });
290
+ console.log('');
291
+ }
292
+ }
240
293
  }
241
294
  // Normal output
242
295
  else {
@@ -245,24 +298,41 @@ async function run(targetPath, options = {}) {
245
298
  const scoreBar = '█'.repeat(Math.floor(result.summary.riskScore / 5)) + '░'.repeat(20 - Math.floor(result.summary.riskScore / 5));
246
299
  console.log(`[SCORE] ${result.summary.riskScore}/100 [${scoreBar}] ${result.summary.riskLevel}\n`);
247
300
 
248
- if (threats.length === 0) {
301
+ if (deduped.length === 0) {
249
302
  console.log('[OK] No threats detected.\n');
250
303
  } else {
251
- console.log(`[ALERT] ${threats.length} threat(s) detected:\n`);
252
- threats.forEach((t, i) => {
253
- console.log(` ${i + 1}. [${t.severity}] ${t.type}`);
304
+ console.log(`[ALERT] ${deduped.length} threat(s) detected:\n`);
305
+ deduped.forEach((t, i) => {
306
+ const countStr = t.count > 1 ? ` (x${t.count})` : '';
307
+ console.log(` ${i + 1}. [${t.severity}] ${t.type}${countStr}`);
254
308
  console.log(` ${t.message}`);
255
309
  console.log(` File: ${t.file}\n`);
256
310
  });
257
311
 
258
312
  console.log('[RESPONSE] Recommendations:\n');
259
- threats.forEach(t => {
313
+ deduped.forEach(t => {
260
314
  const playbook = getPlaybook(t.type);
261
315
  if (playbook) {
262
316
  console.log(` -> ${playbook}\n`);
263
317
  }
264
318
  });
265
319
  }
320
+
321
+ // Sandbox section (normal)
322
+ if (sandboxData) {
323
+ console.log(`[SANDBOX] Dynamic analysis — ${sandboxData.package}`);
324
+ console.log(` Score: ${sandboxData.score}/100`);
325
+ console.log(` Severity: ${sandboxData.severity}`);
326
+ if (sandboxData.findings.length === 0) {
327
+ console.log(' No suspicious behavior detected.\n');
328
+ } else {
329
+ console.log(` ${sandboxData.findings.length} finding(s):`);
330
+ sandboxData.findings.forEach(f => {
331
+ console.log(` [${f.severity}] ${f.type}: ${f.detail}`);
332
+ });
333
+ console.log('');
334
+ }
335
+ }
266
336
  }
267
337
 
268
338
  // Send webhook if configured
@@ -285,8 +355,8 @@ async function run(targetPath, options = {}) {
285
355
  };
286
356
 
287
357
  const levelsToCheck = severityLevels[failLevel] || severityLevels.high;
288
- const failingThreats = threats.filter(t => levelsToCheck.includes(t.severity));
289
-
358
+ const failingThreats = deduped.filter(t => levelsToCheck.includes(t.severity));
359
+
290
360
  return failingThreats.length;
291
361
  }
292
362
 
@@ -17564,7 +17564,7 @@
17564
17564
  "pigS3cr3ts.json"
17565
17565
  ],
17566
17566
  "files": [],
17567
- "updated": "2026-02-09T23:17:51.587Z",
17567
+ "updated": "2026-02-10T20:05:07.831Z",
17568
17568
  "sources": [
17569
17569
  "shai-hulud-detector",
17570
17570
  "datadog-consolidated",
package/src/report.js CHANGED
@@ -91,6 +91,31 @@ function generateHTML(results) {
91
91
  color: #4ecdc4;
92
92
  font-size: 24px;
93
93
  }
94
+ .sandbox-section {
95
+ background: #16213e;
96
+ padding: 20px;
97
+ border-radius: 8px;
98
+ margin-top: 30px;
99
+ border-left: 4px solid #9b59b6;
100
+ }
101
+ .sandbox-section h2 {
102
+ color: #9b59b6;
103
+ margin-top: 0;
104
+ }
105
+ .sandbox-meta {
106
+ display: flex;
107
+ gap: 30px;
108
+ margin-bottom: 15px;
109
+ }
110
+ .sandbox-meta span {
111
+ color: #aaa;
112
+ }
113
+ .sandbox-finding {
114
+ padding: 8px 12px;
115
+ margin: 4px 0;
116
+ border-radius: 4px;
117
+ background: rgba(155, 89, 182, 0.1);
118
+ }
94
119
  </style>
95
120
  </head>
96
121
  <body>
@@ -133,6 +158,24 @@ function generateHTML(results) {
133
158
  </table>
134
159
  ` : '<div class="ok">No threats detected</div>'}
135
160
 
161
+ ${results.sandbox ? `
162
+ <div class="sandbox-section">
163
+ <h2>[SANDBOX] Dynamic Analysis</h2>
164
+ <div class="sandbox-meta">
165
+ <span>Package: <strong>${escapeHtml(results.sandbox.package)}</strong></span>
166
+ <span>Score: <strong>${escapeHtml(String(results.sandbox.score))}/100</strong></span>
167
+ <span>Severity: <strong>${escapeHtml(results.sandbox.severity)}</strong></span>
168
+ </div>
169
+ ${results.sandbox.findings.length === 0
170
+ ? '<p style="color: #4ecdc4;">No suspicious behavior detected.</p>'
171
+ : results.sandbox.findings.map(f => `
172
+ <div class="sandbox-finding">
173
+ <strong>[${escapeHtml(f.severity)}]</strong> ${escapeHtml(f.type)}: ${escapeHtml(f.detail)}
174
+ </div>
175
+ `).join('')}
176
+ </div>
177
+ ` : ''}
178
+
136
179
  <div class="meta">
137
180
  <p>Target: ${escapeHtml(target)}</p>
138
181
  <p>Date: ${escapeHtml(timestamp)}</p>
@@ -363,6 +363,80 @@ const RULES = {
363
363
  references: ['https://docs.github.com/en/actions/security-guides/security-hardening-for-github-actions'],
364
364
  mitre: 'T1195.002'
365
365
  },
366
+
367
+ // Sandbox detections
368
+ sandbox_sensitive_file_read: {
369
+ id: 'MUADDIB-SANDBOX-001',
370
+ name: 'Sandbox: Sensitive File Read',
371
+ severity: 'CRITICAL',
372
+ confidence: 'high',
373
+ description: 'Package reads sensitive credential files during install',
374
+ references: ['https://blog.phylum.io/shai-hulud-npm-worm'],
375
+ mitre: 'T1552.001'
376
+ },
377
+ sandbox_sensitive_file_write: {
378
+ id: 'MUADDIB-SANDBOX-002',
379
+ name: 'Sandbox: Sensitive File Write',
380
+ severity: 'CRITICAL',
381
+ confidence: 'high',
382
+ description: 'Package writes to sensitive credential files during install',
383
+ references: ['https://blog.phylum.io/shai-hulud-npm-worm'],
384
+ mitre: 'T1565.001'
385
+ },
386
+ sandbox_suspicious_filesystem: {
387
+ id: 'MUADDIB-SANDBOX-003',
388
+ name: 'Sandbox: Suspicious Filesystem Change',
389
+ severity: 'HIGH',
390
+ confidence: 'high',
391
+ description: 'Package creates files in suspicious system locations during install',
392
+ references: ['https://attack.mitre.org/techniques/T1543/'],
393
+ mitre: 'T1543'
394
+ },
395
+ sandbox_suspicious_dns: {
396
+ id: 'MUADDIB-SANDBOX-004',
397
+ name: 'Sandbox: Suspicious DNS Query',
398
+ severity: 'HIGH',
399
+ confidence: 'medium',
400
+ description: 'Package resolves non-registry domain during install',
401
+ references: ['https://attack.mitre.org/techniques/T1071/'],
402
+ mitre: 'T1071'
403
+ },
404
+ sandbox_suspicious_connection: {
405
+ id: 'MUADDIB-SANDBOX-005',
406
+ name: 'Sandbox: Suspicious Network Connection',
407
+ severity: 'HIGH',
408
+ confidence: 'medium',
409
+ description: 'Package makes TCP connection to non-registry host during install',
410
+ references: ['https://attack.mitre.org/techniques/T1071/'],
411
+ mitre: 'T1071'
412
+ },
413
+ sandbox_suspicious_process: {
414
+ id: 'MUADDIB-SANDBOX-006',
415
+ name: 'Sandbox: Dangerous Process Spawned',
416
+ severity: 'CRITICAL',
417
+ confidence: 'high',
418
+ description: 'Package spawns dangerous command during install (curl, wget, nc, etc.)',
419
+ references: ['https://attack.mitre.org/techniques/T1059/'],
420
+ mitre: 'T1059'
421
+ },
422
+ sandbox_unknown_process: {
423
+ id: 'MUADDIB-SANDBOX-007',
424
+ name: 'Sandbox: Unknown Process Spawned',
425
+ severity: 'MEDIUM',
426
+ confidence: 'low',
427
+ description: 'Package spawns unrecognized process during install',
428
+ references: ['https://attack.mitre.org/techniques/T1059/'],
429
+ mitre: 'T1059'
430
+ },
431
+ sandbox_timeout: {
432
+ id: 'MUADDIB-SANDBOX-008',
433
+ name: 'Sandbox: Container Timeout',
434
+ severity: 'CRITICAL',
435
+ confidence: 'high',
436
+ description: 'Package install exceeded sandbox timeout - possible infinite loop or resource exhaustion',
437
+ references: ['https://attack.mitre.org/techniques/T1499/'],
438
+ mitre: 'T1499'
439
+ },
366
440
  };
367
441
 
368
442
  function getRule(type) {
package/src/sandbox.js CHANGED
@@ -1,158 +1,295 @@
1
- const { spawn } = require('child_process');
1
+ const { execSync, spawn } = require('child_process');
2
2
  const path = require('path');
3
3
 
4
4
  const DOCKER_IMAGE = 'muaddib-sandbox';
5
+ const CONTAINER_TIMEOUT = 120000; // 120 seconds
6
+
7
+ // Domains excluded from network findings (false positives)
8
+ const SAFE_DOMAINS = [
9
+ 'registry.npmjs.org',
10
+ 'github.com',
11
+ 'objects.githubusercontent.com'
12
+ ];
13
+
14
+ // IPs/ports excluded from connection findings (false positives)
15
+ const SAFE_IPS = ['127.0.0.1', '0.0.0.0'];
16
+ const PROBE_PORTS = [65535]; // Node.js internal connectivity checks
17
+
18
+ // Commands that are always suspicious in a sandbox
19
+ const DANGEROUS_CMDS = ['curl', 'wget', 'nc', 'netcat', 'python', 'python3', 'bash', 'sh'];
20
+
21
+ // ── Docker availability checks ──
22
+
23
+ function isDockerAvailable() {
24
+ try {
25
+ execSync('docker info', { stdio: 'pipe', timeout: 10000 });
26
+ return true;
27
+ } catch {
28
+ return false;
29
+ }
30
+ }
31
+
32
+ function imageExists() {
33
+ try {
34
+ execSync(`docker image inspect ${DOCKER_IMAGE}`, { stdio: 'pipe', timeout: 10000 });
35
+ return true;
36
+ } catch {
37
+ return false;
38
+ }
39
+ }
40
+
41
+ // ── Build image (with cache) ──
5
42
 
6
43
  async function buildSandboxImage() {
44
+ if (!isDockerAvailable()) {
45
+ console.log('[SANDBOX] Docker is not installed or not running. Skipping sandbox analysis.');
46
+ return false;
47
+ }
48
+
49
+ if (imageExists()) {
50
+ console.log('[SANDBOX] Using cached Docker image.');
51
+ return true;
52
+ }
53
+
7
54
  console.log('[SANDBOX] Building Docker image...');
8
-
9
- return new Promise((resolve, reject) => {
55
+
56
+ return new Promise((resolve) => {
10
57
  const dockerfilePath = path.join(__dirname, '..', 'docker');
11
58
  const proc = spawn('docker', ['build', '-t', DOCKER_IMAGE, dockerfilePath], {
12
59
  stdio: 'inherit'
13
60
  });
14
-
61
+
15
62
  proc.on('close', (code) => {
16
63
  if (code === 0) {
17
64
  console.log('[SANDBOX] Image built successfully.');
18
- resolve();
65
+ resolve(true);
19
66
  } else {
20
- reject(new Error(`Docker build failed with code ${code}`));
67
+ console.log('[SANDBOX] Docker build failed.');
68
+ resolve(false);
21
69
  }
22
70
  });
23
-
24
- proc.on('error', reject);
71
+
72
+ proc.on('error', () => {
73
+ console.log('[SANDBOX] Docker error during build.');
74
+ resolve(false);
75
+ });
25
76
  });
26
77
  }
27
78
 
79
+ // ── Run sandbox analysis ──
80
+
28
81
  async function runSandbox(packageName) {
29
- console.log(`\n[SANDBOX] Analyzing "${packageName}" in isolated container...\n`);
30
-
31
- const results = {
32
- package: packageName,
33
- timestamp: new Date().toISOString(),
34
- network: [],
35
- processes: [],
36
- fileAccess: [],
37
- suspicious: false,
38
- threats: [],
39
- exitCode: null,
40
- rawOutput: ''
41
- };
42
-
43
- return new Promise((resolve, reject) => {
82
+ const cleanResult = { score: 0, severity: 'CLEAN', findings: [], raw_report: null, suspicious: false };
83
+
84
+ if (!isDockerAvailable()) {
85
+ console.log('[SANDBOX] Docker is not installed or not running. Skipping.');
86
+ return cleanResult;
87
+ }
88
+
89
+ console.log(`[SANDBOX] Analyzing "${packageName}" in isolated container...`);
90
+
91
+ return new Promise((resolve) => {
92
+ let stdout = '';
93
+ let timedOut = false;
94
+ const containerName = `muaddib-sandbox-${Date.now()}`;
95
+
44
96
  const proc = spawn('docker', [
45
97
  'run',
46
98
  '--rm',
99
+ `--name=${containerName}`,
47
100
  '--network=bridge',
48
101
  '--memory=512m',
49
102
  '--cpus=1',
50
103
  '--pids-limit=100',
104
+ '--cap-drop=ALL',
105
+ '--cap-add=SYS_PTRACE',
106
+ '--security-opt', 'no-new-privileges',
51
107
  DOCKER_IMAGE,
52
108
  packageName
53
109
  ]);
54
-
55
- let currentSection = null;
56
110
 
57
- proc.stdout.on('data', (data) => {
58
- const text = data.toString();
59
- results.rawOutput += text;
60
- process.stdout.write(text);
61
-
62
- // Parse sections
63
- if (text.includes('=== NETWORK CONNECTIONS ===')) {
64
- currentSection = 'network';
65
- } else if (text.includes('=== PROCESS SPAWNS ===')) {
66
- currentSection = 'processes';
67
- } else if (text.includes('=== FILE ACCESS ===')) {
68
- currentSection = 'fileAccess';
69
- } else if (currentSection && text.trim()) {
70
- results[currentSection].push(text.trim());
111
+ // Timeout: kill container after 120s
112
+ const timer = setTimeout(() => {
113
+ timedOut = true;
114
+ console.log('[SANDBOX] Timeout (120s). Killing container...');
115
+ try {
116
+ execSync(`docker kill ${containerName}`, { stdio: 'pipe', timeout: 5000 });
117
+ } catch {
118
+ proc.kill('SIGKILL');
71
119
  }
120
+ }, CONTAINER_TIMEOUT);
121
+
122
+ proc.stdout.on('data', (data) => {
123
+ stdout += data.toString();
72
124
  });
73
-
125
+
74
126
  proc.stderr.on('data', (data) => {
75
- process.stderr.write(data);
76
- });
77
-
78
- proc.on('close', (code) => {
79
- // Store exit code
80
- results.exitCode = code;
81
-
82
- // Analyze results
83
- results.suspicious = analyzeResults(results);
84
-
85
- if (results.suspicious) {
86
- console.log('\n[SANDBOX] ⚠️ SUSPICIOUS BEHAVIOR DETECTED!\n');
87
- for (const threat of results.threats) {
88
- console.log(` [${threat.severity}] ${threat.message}`);
127
+ // Forward sandbox progress logs
128
+ const text = data.toString();
129
+ for (const line of text.split('\n')) {
130
+ if (line.includes('[SANDBOX]')) {
131
+ console.log(line.trim());
89
132
  }
90
- } else {
91
- console.log('\n[SANDBOX] ✓ No suspicious behavior detected.\n');
92
133
  }
93
-
94
- resolve(results);
95
134
  });
96
-
135
+
136
+ proc.on('close', () => {
137
+ clearTimeout(timer);
138
+
139
+ if (timedOut) {
140
+ const result = {
141
+ score: 100,
142
+ severity: 'CRITICAL',
143
+ findings: [{
144
+ type: 'timeout',
145
+ severity: 'CRITICAL',
146
+ detail: 'Container exceeded 120s timeout',
147
+ evidence: `Killed after ${CONTAINER_TIMEOUT}ms`
148
+ }],
149
+ raw_report: null,
150
+ suspicious: true
151
+ };
152
+ displayResults(result);
153
+ resolve(result);
154
+ return;
155
+ }
156
+
157
+ // Parse JSON from container stdout
158
+ let report;
159
+ try {
160
+ report = JSON.parse(stdout);
161
+ } catch {
162
+ console.log('[SANDBOX] Failed to parse container output.');
163
+ resolve(cleanResult);
164
+ return;
165
+ }
166
+
167
+ const { score, findings } = scoreFindings(report);
168
+ const severity = getSeverity(score);
169
+ const result = { score, severity, findings, raw_report: report, suspicious: score > 0 };
170
+
171
+ displayResults(result);
172
+ resolve(result);
173
+ });
174
+
97
175
  proc.on('error', (err) => {
98
- if (err.message.includes('ENOENT')) {
99
- reject(new Error('Docker not found. Please install Docker Desktop.'));
176
+ clearTimeout(timer);
177
+ if (err.code === 'ENOENT') {
178
+ console.log('[SANDBOX] Docker not found. Please install Docker.');
100
179
  } else {
101
- reject(err);
180
+ console.log(`[SANDBOX] Error: ${err.message}`);
102
181
  }
182
+ resolve(cleanResult);
103
183
  });
104
184
  });
105
185
  }
106
186
 
107
- function analyzeResults(results) {
108
- let suspicious = false;
109
-
110
- // Check for suspicious network connections
111
- const suspiciousHosts = ['pastebin', 'discord.com/api/webhooks', 'ngrok', 'burpcollaborator'];
112
- for (const conn of results.network) {
113
- for (const host of suspiciousHosts) {
114
- if (conn.toLowerCase().includes(host)) {
115
- results.threats.push({
116
- severity: 'CRITICAL',
117
- type: 'suspicious_network',
118
- message: `Connection to suspicious host: ${host}`
119
- });
120
- suspicious = true;
121
- }
187
+ // ── Scoring engine ──
188
+
189
+ function scoreFindings(report) {
190
+ let score = 0;
191
+ const findings = [];
192
+
193
+ // 1. Sensitive file reads
194
+ for (const file of (report.sensitive_files?.read || [])) {
195
+ if (/\.npmrc/.test(file) || /\.ssh/.test(file) || /\.aws/.test(file)) {
196
+ score += 40;
197
+ findings.push({ type: 'sensitive_file_read', severity: 'CRITICAL', detail: `Read credential file: ${file}`, evidence: file });
198
+ } else if (/\/etc\/passwd/.test(file) || /\/etc\/shadow/.test(file)) {
199
+ score += 25;
200
+ findings.push({ type: 'sensitive_file_read', severity: 'HIGH', detail: `Read system file: ${file}`, evidence: file });
201
+ } else if (/\.env/.test(file) || /\.gitconfig/.test(file) || /\.bash_history/.test(file)) {
202
+ score += 15;
203
+ findings.push({ type: 'sensitive_file_read', severity: 'MEDIUM', detail: `Read config file: ${file}`, evidence: file });
122
204
  }
123
205
  }
124
-
125
- // Check for credential file access
126
- const sensitiveFiles = ['.npmrc', '.ssh', '.aws', '.gitconfig', '.env'];
127
- for (const access of results.fileAccess) {
128
- for (const file of sensitiveFiles) {
129
- if (access.includes(file)) {
130
- results.threats.push({
131
- severity: 'HIGH',
132
- type: 'credential_access',
133
- message: `Access to sensitive file: ${file}`
134
- });
135
- suspicious = true;
136
- }
206
+
207
+ // 2. Sensitive file writes (from strace)
208
+ for (const file of (report.sensitive_files?.written || [])) {
209
+ if (/\.npmrc/.test(file) || /\.ssh/.test(file) || /\.aws/.test(file)) {
210
+ score += 40;
211
+ findings.push({ type: 'sensitive_file_write', severity: 'CRITICAL', detail: `Write to credential file: ${file}`, evidence: file });
212
+ } else if (/\/etc\/passwd/.test(file) || /\/etc\/shadow/.test(file)) {
213
+ score += 25;
214
+ findings.push({ type: 'sensitive_file_write', severity: 'HIGH', detail: `Write to system file: ${file}`, evidence: file });
215
+ } else {
216
+ score += 15;
217
+ findings.push({ type: 'sensitive_file_write', severity: 'MEDIUM', detail: `Write to sensitive file: ${file}`, evidence: file });
137
218
  }
138
219
  }
139
-
140
- // Check for suspicious process spawns
141
- const suspiciousProcesses = ['curl ', 'wget ', '/nc ', 'netcat', 'bash -c', 'sh -c', 'powershell'];
142
- for (const proc of results.processes) {
143
- for (const suspicious_proc of suspiciousProcesses) {
144
- if (proc.toLowerCase().includes(suspicious_proc)) {
145
- results.threats.push({
146
- severity: 'HIGH',
147
- type: 'suspicious_process',
148
- message: `Suspicious process spawn: ${suspicious_proc}`
149
- });
150
- suspicious = true;
151
- }
220
+
221
+ // 3. Filesystem changes — files created in suspicious locations
222
+ for (const file of (report.filesystem?.created || [])) {
223
+ if (/^\/usr\/bin\//.test(file) || /crontab/.test(file) || /\/cron\.d\//.test(file)) {
224
+ score += 50;
225
+ findings.push({ type: 'suspicious_filesystem', severity: 'CRITICAL', detail: `File created in system path: ${file}`, evidence: file });
226
+ } else if (/^\/tmp\//.test(file)) {
227
+ score += 30;
228
+ findings.push({ type: 'suspicious_filesystem', severity: 'HIGH', detail: `File created in /tmp: ${file}`, evidence: file });
229
+ }
230
+ }
231
+
232
+ // 4. DNS queries (exclude safe domains)
233
+ for (const domain of (report.network?.dns_queries || [])) {
234
+ if (isSafeDomain(domain)) continue;
235
+ score += 20;
236
+ findings.push({ type: 'suspicious_dns', severity: 'HIGH', detail: `DNS query to non-registry domain: ${domain}`, evidence: domain });
237
+ }
238
+
239
+ // 5. TCP connections (exclude safe hosts, probe ports, localhost)
240
+ for (const conn of (report.network?.http_connections || [])) {
241
+ if (isSafeHost(conn.host)) continue;
242
+ if (SAFE_IPS.includes(conn.host)) continue;
243
+ if (PROBE_PORTS.includes(conn.port)) continue;
244
+ score += 25;
245
+ findings.push({ type: 'suspicious_connection', severity: 'HIGH', detail: `TCP connection to ${conn.host}:${conn.port}`, evidence: `${conn.host}:${conn.port}` });
246
+ }
247
+
248
+ // 6. Suspicious processes
249
+ for (const p of (report.processes?.spawned || [])) {
250
+ const cmd = p.command || '';
251
+ const basename = cmd.split('/').pop();
252
+ if (DANGEROUS_CMDS.some(d => basename === d)) {
253
+ score += 40;
254
+ findings.push({ type: 'suspicious_process', severity: 'CRITICAL', detail: `Dangerous command spawned: ${cmd}`, evidence: cmd });
255
+ } else if (cmd) {
256
+ score += 15;
257
+ findings.push({ type: 'unknown_process', severity: 'MEDIUM', detail: `Unknown process spawned: ${cmd}`, evidence: cmd });
258
+ }
259
+ }
260
+
261
+ score = Math.min(100, score);
262
+ return { score, findings };
263
+ }
264
+
265
+ // ── Helpers ──
266
+
267
+ function isSafeDomain(domain) {
268
+ return SAFE_DOMAINS.some(safe => domain === safe || domain.endsWith('.' + safe));
269
+ }
270
+
271
+ function isSafeHost(host) {
272
+ return SAFE_DOMAINS.some(safe => host === safe || host.endsWith('.' + safe));
273
+ }
274
+
275
+ function getSeverity(score) {
276
+ if (score === 0) return 'CLEAN';
277
+ if (score <= 20) return 'LOW';
278
+ if (score <= 50) return 'MEDIUM';
279
+ if (score <= 80) return 'HIGH';
280
+ return 'CRITICAL';
281
+ }
282
+
283
+ function displayResults(result) {
284
+ console.log(`\n[SANDBOX] Score: ${result.score}/100 — ${result.severity}`);
285
+ if (result.findings.length === 0) {
286
+ console.log('[SANDBOX] No suspicious behavior detected.');
287
+ } else {
288
+ console.log(`[SANDBOX] ${result.findings.length} finding(s):`);
289
+ for (const f of result.findings) {
290
+ console.log(` [${f.severity}] ${f.type}: ${f.detail}`);
152
291
  }
153
292
  }
154
-
155
- return suspicious;
156
293
  }
157
294
 
158
- module.exports = { buildSandboxImage, runSandbox };
295
+ module.exports = { buildSandboxImage, runSandbox };
@@ -24,10 +24,15 @@ const SENSITIVE_STRINGS = [
24
24
  'Goldox-T3chs'
25
25
  ];
26
26
 
27
+ // Env vars that are safe and should NOT be flagged (common config/runtime vars)
28
+ const SAFE_ENV_VARS = [
29
+ 'NODE_ENV', 'PORT', 'HOST', 'HOSTNAME', 'PWD', 'HOME', 'PATH',
30
+ 'LANG', 'TERM', 'CI', 'DEBUG', 'VERBOSE', 'LOG_LEVEL'
31
+ ];
32
+
27
33
  // Env var keywords to detect sensitive environment access (separate from SENSITIVE_STRINGS)
28
- const ENV_SENSITIVE_VARS = [
29
- 'TOKEN', 'SECRET', 'KEY', 'PASSWORD', 'CREDENTIAL',
30
- 'AUTH', 'NPM', 'AWS', 'GITHUB', 'SSH', 'NPMRC'
34
+ const ENV_SENSITIVE_KEYWORDS = [
35
+ 'TOKEN', 'SECRET', 'KEY', 'PASSWORD', 'CREDENTIAL', 'AUTH'
31
36
  ];
32
37
 
33
38
  // Strings that are NOT suspicious
@@ -134,14 +139,32 @@ function analyzeFile(content, filePath, basePath) {
134
139
  node.object?.object?.name === 'process' &&
135
140
  node.object?.property?.name === 'env'
136
141
  ) {
137
- const envVar = node.property?.name;
138
- if (envVar && ENV_SENSITIVE_VARS.some(s => envVar.toUpperCase().includes(s))) {
142
+ // Dynamic access: process.env[variable] — always flag as MEDIUM
143
+ if (node.computed) {
139
144
  threats.push({
140
145
  type: 'env_access',
141
- severity: 'HIGH',
142
- message: `Access to sensitive variable process.env.${envVar}.`,
146
+ severity: 'MEDIUM',
147
+ message: 'Dynamic access to process.env (variable key).',
143
148
  file: path.relative(basePath, filePath)
144
149
  });
150
+ return;
151
+ }
152
+
153
+ const envVar = node.property?.name;
154
+ if (envVar) {
155
+ // Skip safe/common env vars
156
+ if (SAFE_ENV_VARS.includes(envVar)) {
157
+ return;
158
+ }
159
+ // Flag only vars containing sensitive keywords
160
+ if (ENV_SENSITIVE_KEYWORDS.some(s => envVar.toUpperCase().includes(s))) {
161
+ threats.push({
162
+ type: 'env_access',
163
+ severity: 'HIGH',
164
+ message: `Access to sensitive variable process.env.${envVar}.`,
165
+ file: path.relative(basePath, filePath)
166
+ });
167
+ }
145
168
  }
146
169
  }
147
170
  }