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 +1 -1
- package/docker/sandbox-runner.sh +144 -16
- package/package.json +1 -1
- package/src/index.js +56 -2
- package/src/ioc/data/iocs.json +17578 -17578
- package/src/report.js +43 -0
- package/src/rules/index.js +74 -0
- package/src/sandbox.js +243 -106
- package/src/scanner/npm-registry.js +81 -0
- package/src/scanner/shell.js +1 -1
- package/src/scanner/typosquat.js +136 -13
- package/src/utils.js +2 -1
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
|
@@ -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
|