muaddib-scanner 1.4.3 → 1.6.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/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.3",
3
+ "version": "1.6.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
@@ -80,7 +80,7 @@ function scanParanoid(targetPath) {
80
80
  }
81
81
 
82
82
  function walkDir(dir) {
83
- const excluded = ['node_modules', '.git', 'test', 'tests', 'src', 'vscode-extension', '.muaddib-cache', 'data', 'iocs'];
83
+ const excluded = ['node_modules', '.git', 'test', 'tests', 'src', 'vscode-extension', '.muaddib-cache', 'data', 'iocs', 'docker'];
84
84
  try {
85
85
  const files = fs.readdirSync(dir);
86
86
  for (const file of files) {
@@ -150,6 +150,27 @@ 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
+
153
174
  // Deduplicate: same file + same type + same message = show once with count
154
175
  const deduped = [];
155
176
  const seen = new Map();
@@ -209,7 +230,8 @@ async function run(targetPath, options = {}) {
209
230
  low: lowCount,
210
231
  riskScore: riskScore,
211
232
  riskLevel: riskLevel
212
- }
233
+ },
234
+ sandbox: sandboxData
213
235
  };
214
236
 
215
237
  // JSON output
@@ -252,6 +274,22 @@ async function run(targetPath, options = {}) {
252
274
  console.log('');
253
275
  });
254
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
+ }
255
293
  }
256
294
  // Normal output
257
295
  else {
@@ -279,6 +317,22 @@ async function run(targetPath, options = {}) {
279
317
  }
280
318
  });
281
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
+ }
282
336
  }
283
337
 
284
338
  // Send webhook if configured