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 +6 -1
- package/docker/Dockerfile +1 -1
- package/docker/sandbox-runner.sh +144 -16
- package/package.json +1 -1
- package/src/index.js +86 -16
- package/src/ioc/data/iocs.json +1 -1
- package/src/report.js +43 -0
- package/src/rules/index.js +74 -0
- package/src/sandbox.js +243 -106
- package/src/scanner/ast.js +30 -7
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 (
|
|
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
package/docker/sandbox-runner.sh
CHANGED
|
@@ -1,26 +1,154 @@
|
|
|
1
1
|
#!/bin/sh
|
|
2
|
-
PACKAGE
|
|
2
|
+
PACKAGE="$1"
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
if [ -z "$PACKAGE" ]; then
|
|
5
|
+
echo "Usage: sandbox-runner.sh <package-name>" >&2
|
|
6
|
+
exit 1
|
|
7
|
+
fi
|
|
5
8
|
|
|
6
|
-
|
|
7
|
-
|
|
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
|
-
#
|
|
11
|
-
|
|
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
|
-
|
|
14
|
-
kill $TCPDUMP_PID 2>/dev/null
|
|
102
|
+
INSTALL_OUTPUT=$(head -c 5000 /tmp/install.log)
|
|
15
103
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
|
|
21
|
-
|
|
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
|
-
|
|
24
|
-
|
|
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
|
-
|
|
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
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 =
|
|
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 =
|
|
169
|
-
const highCount =
|
|
170
|
-
const mediumCount =
|
|
171
|
-
const lowCount =
|
|
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:
|
|
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
|
-
|
|
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 (
|
|
301
|
+
if (deduped.length === 0) {
|
|
249
302
|
console.log('[OK] No threats detected.\n');
|
|
250
303
|
} else {
|
|
251
|
-
console.log(`[ALERT] ${
|
|
252
|
-
|
|
253
|
-
|
|
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
|
-
|
|
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 =
|
|
289
|
-
|
|
358
|
+
const failingThreats = deduped.filter(t => levelsToCheck.includes(t.severity));
|
|
359
|
+
|
|
290
360
|
return failingThreats.length;
|
|
291
361
|
}
|
|
292
362
|
|
package/src/ioc/data/iocs.json
CHANGED
|
@@ -17564,7 +17564,7 @@
|
|
|
17564
17564
|
"pigS3cr3ts.json"
|
|
17565
17565
|
],
|
|
17566
17566
|
"files": [],
|
|
17567
|
-
"updated": "2026-02-
|
|
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>
|
package/src/rules/index.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
67
|
+
console.log('[SANDBOX] Docker build failed.');
|
|
68
|
+
resolve(false);
|
|
21
69
|
}
|
|
22
70
|
});
|
|
23
|
-
|
|
24
|
-
proc.on('error',
|
|
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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
-
|
|
99
|
-
|
|
176
|
+
clearTimeout(timer);
|
|
177
|
+
if (err.code === 'ENOENT') {
|
|
178
|
+
console.log('[SANDBOX] Docker not found. Please install Docker.');
|
|
100
179
|
} else {
|
|
101
|
-
|
|
180
|
+
console.log(`[SANDBOX] Error: ${err.message}`);
|
|
102
181
|
}
|
|
182
|
+
resolve(cleanResult);
|
|
103
183
|
});
|
|
104
184
|
});
|
|
105
185
|
}
|
|
106
186
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|
-
//
|
|
126
|
-
const
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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
|
-
//
|
|
141
|
-
const
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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 };
|
package/src/scanner/ast.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
138
|
-
if (
|
|
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: '
|
|
142
|
-
message:
|
|
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
|
}
|