muaddib-scanner 2.2.19 → 2.2.22

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.
@@ -1,292 +1,313 @@
1
- #!/bin/sh
2
- PACKAGE="$1"
3
- MODE="${2:-permissive}"
4
-
5
- if [ -z "$PACKAGE" ]; then
6
- echo "Usage: sandbox-runner.sh <package-name> [permissive|strict]" >&2
7
- exit 1
8
- fi
9
-
10
- TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
11
- START_MS=$(date +%s%3N 2>/dev/null || echo 0)
12
-
13
- # ── 0. Strict mode: iptables rules ──
14
- if [ "$MODE" = "strict" ]; then
15
- echo "[SANDBOX] STRICT MODE blocking non-essential network..." >&2
16
- # Allow loopback
17
- iptables -A OUTPUT -o lo -j ACCEPT 2>/dev/null
18
- # Allow DNS (UDP 53)
19
- iptables -A OUTPUT -p udp --dport 53 -j ACCEPT 2>/dev/null
20
- # Allow registry.npmjs.org (resolve + allow)
21
- REGISTRY_IPS=$(nslookup registry.npmjs.org 2>/dev/null | grep -E '^Address:' | tail -n+2 | awk '{print $2}')
22
- for ip in $REGISTRY_IPS; do
23
- iptables -A OUTPUT -d "$ip" -p tcp --dport 443 -j ACCEPT 2>/dev/null
24
- done
25
- # Log + reject everything else
26
- iptables -A OUTPUT -j LOG --log-prefix "BLOCKED: " 2>/dev/null
27
- iptables -A OUTPUT -j REJECT 2>/dev/null
28
- echo "[SANDBOX] iptables rules applied." >&2
29
- fi
30
-
31
- # ── 1. Filesystem snapshot BEFORE install ──
32
- echo "[SANDBOX] Snapshot filesystem before install..." >&2
33
- find / -type f 2>/dev/null | sort > /tmp/fs-before.txt
34
-
35
- # ── 2. tcpdump: separate captures for DNS, HTTP, TLS ──
36
- echo "[SANDBOX] Starting network capture..." >&2
37
- tcpdump -i any -nn 'port 53' -l > /tmp/dns.log 2>/dev/null &
38
- DNS_PID=$!
39
- tcpdump -i any -nn -A 'tcp port 80' -l > /tmp/http.log 2>/dev/null &
40
- HTTP_PID=$!
41
- tcpdump -i any -nn 'tcp port 443' -l > /tmp/tls.log 2>/dev/null &
42
- TLS_PID=$!
43
- tcpdump -i any -nn 'not port 53 and not port 80 and not port 443' -l > /tmp/other.log 2>/dev/null &
44
- OTHER_PID=$!
45
- sleep 1
46
-
47
- # ── 2b. CI environment simulation ──
48
- # Simulate CI to trigger CI-aware malware that checks for these env vars
49
- echo "[SANDBOX] Simulating CI environment..." >&2
50
- export CI=true
51
- export GITHUB_ACTIONS=true
52
- export GITLAB_CI=true
53
- export TRAVIS=true
54
- export CIRCLECI=true
55
- export JENKINS_URL=http://localhost:8080
56
-
57
- # ── 2c. Canary tokens (honeypots) ──
58
- # Use Docker-injected dynamic tokens if available, otherwise static fallbacks.
59
- # If exfiltrated via network/DNS/files, sandbox.js detects the theft.
60
- export GITHUB_TOKEN="${GITHUB_TOKEN:-MUADDIB_CANARY_GITHUB_f8k3t0k3n}"
61
- export NPM_TOKEN="${NPM_TOKEN:-MUADDIB_CANARY_NPM_s3cr3tt0k3n}"
62
- export AWS_ACCESS_KEY_ID="${AWS_ACCESS_KEY_ID:-MUADDIB_CANARY_AKIAIOSFODNN7EXAMPLE}"
63
- export AWS_SECRET_ACCESS_KEY="${AWS_SECRET_ACCESS_KEY:-MUADDIB_CANARY_wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY}"
64
- export SLACK_WEBHOOK_URL="${SLACK_WEBHOOK_URL:-https://hooks.slack.com/MUADDIB_CANARY_SLACK}"
65
- export DISCORD_WEBHOOK_URL="${DISCORD_WEBHOOK_URL:-https://discord.com/api/webhooks/MUADDIB_CANARY_DISCORD}"
66
-
67
- # ── 3. npm install with strace ──
68
- echo "[SANDBOX] Installing $PACKAGE..." >&2
69
- cd /sandbox/install
70
- strace -f -e trace=network,process,open,openat,connect,execve,sendto,recvfrom \
71
- -o /tmp/strace.log \
72
- npm install "$PACKAGE" --ignore-scripts=false > /tmp/install.log 2>&1
73
- EXIT_CODE=$?
74
-
75
- # ── 4. Filesystem snapshot AFTER install ──
76
- echo "[SANDBOX] Snapshot filesystem after install..." >&2
77
- find / -type f 2>/dev/null | sort > /tmp/fs-after.txt
78
-
79
- # Stop tcpdump
80
- kill "$DNS_PID" "$HTTP_PID" "$TLS_PID" "$OTHER_PID" 2>/dev/null
81
- wait "$DNS_PID" "$HTTP_PID" "$TLS_PID" "$OTHER_PID" 2>/dev/null
82
-
83
- END_MS=$(date +%s%3N 2>/dev/null || echo 0)
84
- DURATION_MS=$((END_MS - START_MS))
85
- [ "$DURATION_MS" -lt 0 ] 2>/dev/null && DURATION_MS=0
86
-
87
- # ── 5. Filesystem diff ──
88
- echo "[SANDBOX] Analyzing filesystem changes..." >&2
89
- comm -13 /tmp/fs-before.txt /tmp/fs-after.txt | grep -v '^/sandbox/install/' | grep -v '^/tmp/' > /tmp/fs-created.txt
90
- comm -23 /tmp/fs-before.txt /tmp/fs-after.txt | grep -v '^/sandbox/install/' > /tmp/fs-deleted.txt
91
-
92
- # ── 6. Parse strace ──
93
- echo "[SANDBOX] Parsing strace..." >&2
94
-
95
- SENSITIVE='\.npmrc|\.ssh/|\.aws/|\.env|/etc/passwd|/etc/shadow|\.gitconfig|\.bash_history'
96
-
97
- # 6a. Sensitive file access (read)
98
- grep -E 'openat\(' /tmp/strace.log 2>/dev/null | \
99
- grep -E "$SENSITIVE" | \
100
- grep 'O_RDONLY' | \
101
- sed 's/.*openat([^,]*, "\([^"]*\)".*/\1/' | \
102
- sort -u > /tmp/sensitive-read.txt
103
-
104
- # 6b. Sensitive file access (write)
105
- grep -E 'openat\(' /tmp/strace.log 2>/dev/null | \
106
- grep -E "$SENSITIVE" | \
107
- grep -E 'O_WRONLY|O_RDWR|O_CREAT' | \
108
- sed 's/.*openat([^,]*, "\([^"]*\)".*/\1/' | \
109
- sort -u > /tmp/sensitive-written.txt
110
-
111
- # 6c. Suspicious execve
112
- grep 'execve(' /tmp/strace.log 2>/dev/null | \
113
- grep '= 0' | \
114
- grep -vE 'execve\("[^"]*/(node|npm|npx|sh|git)"' | \
115
- sed -n 's/.*\[pid \([0-9]*\)\].*execve("\([^"]*\)".*/\1\t\2/p' > /tmp/suspicious-cmds.txt
116
-
117
- grep 'execve(' /tmp/strace.log 2>/dev/null | \
118
- grep '= 0' | \
119
- grep -vE 'execve\("[^"]*/(node|npm|npx|sh|git)"' | \
120
- grep -v '\[pid' | \
121
- sed -n 's/.*execve("\([^"]*\)".*/0\t\1/p' >> /tmp/suspicious-cmds.txt
122
-
123
- # 6d. Outgoing connections (AF_INET, successful)
124
- grep 'connect(' /tmp/strace.log 2>/dev/null | \
125
- grep 'AF_INET' | grep -v 'AF_INET6' | \
126
- grep '= 0' | \
127
- sed -n 's/.*sin_port=htons(\([0-9]*\)).*sin_addr=inet_addr("\([^"]*\)").*/\2\t\1/p' | \
128
- grep -v ' 65535$' | \
129
- grep -v '^127\.' | \
130
- sort -u > /tmp/connections.txt
131
-
132
- # ── 7. Parse DNS resolutions (query → answer pairs) ──
133
- echo "[SANDBOX] Parsing DNS resolutions..." >&2
134
-
135
- # Extract DNS queries and answers from dns.log
136
- # Format: "domain query_type answer_ip"
137
- awk '
138
- /A\?/ { domain=$0; sub(/.*A\? /,"",domain); sub(/ .*$/,"",domain); gsub(/\.$/,"",domain); pending=domain }
139
- /A [0-9]+\.[0-9]+\.[0-9]+\.[0-9]+/ && pending {
140
- ip=$0; sub(/.*A /,"",ip); sub(/ .*$/,"",ip);
141
- print pending "\t" ip;
142
- pending=""
143
- }
144
- ' /tmp/dns.log 2>/dev/null | sort -u > /tmp/dns-resolutions.txt
145
-
146
- # Plain DNS query list (for backward compat)
147
- grep -oE '(A|AAAA)\? [^ ]+' /tmp/dns.log 2>/dev/null | \
148
- awk '{print $2}' | sed 's/\.$//' | sort -u > /tmp/dns-queries.txt
149
-
150
- # ── 8. Parse HTTP requests ──
151
- echo "[SANDBOX] Parsing HTTP requests..." >&2
152
-
153
- # Extract HTTP method, host, path from http.log
154
- awk '
155
- /^[0-9].*length [0-9]/ { next }
156
- /(GET|POST|PUT|DELETE|PATCH) \// {
157
- method=$0; sub(/.*((GET|POST|PUT|DELETE|PATCH)) /,"",method);
158
- path=method; sub(/ .*$/,"",path);
159
- meth=$0; match(meth,/(GET|POST|PUT|DELETE|PATCH)/); meth=substr(meth,RSTART,RLENGTH);
160
- pending_method=meth; pending_path=path; pending_host=""; pending_body=""
161
- }
162
- /^Host:/ && pending_method { pending_host=$2; gsub(/\r/,"",pending_host) }
163
- /^\r?$/ && pending_method && pending_host {
164
- print pending_method "\t" pending_host "\t" pending_path;
165
- pending_method=""; pending_host=""; pending_path=""
166
- }
167
- ' /tmp/http.log 2>/dev/null > /tmp/http-requests.txt
168
-
169
- # Extract HTTP body snippets for exfiltration detection
170
- awk '
171
- /^(POST|PUT|PATCH) / { capturing=1; body="" }
172
- capturing && /^[^\t]/ && !/^(GET|POST|PUT|DELETE|PATCH|HTTP|Host:|Content|Accept|User-Agent|Connection)/ {
173
- if (length(body) < 500) body = body $0
174
- }
175
- /^\r?$/ && capturing { if (length(body)>0) print body; capturing=0; body="" }
176
- ' /tmp/http.log 2>/dev/null > /tmp/http-bodies.txt
177
-
178
- # ── 9. Parse TLS connections (SNI via IP correlation) ──
179
- echo "[SANDBOX] Parsing TLS connections..." >&2
180
-
181
- # Map IPs to domains from DNS resolutions, then correlate with TLS IPs
182
- awk '{print $1}' /tmp/connections.txt 2>/dev/null | sort -u > /tmp/tls-ips.txt
183
-
184
- # Build domain→IP map, then find TLS connections
185
- awk -F'\t' '
186
- NR==FNR { ip_domain[$2]=$1; next }
187
- { if ($1 in ip_domain) print ip_domain[$1] "\t" $1 "\t" $2 }
188
- ' /tmp/dns-resolutions.txt /tmp/connections.txt 2>/dev/null | \
189
- grep ' 443$' | sort -u > /tmp/tls-connections.txt
190
-
191
- # ── 10. Blocked connections (strict mode) ──
192
- if [ "$MODE" = "strict" ]; then
193
- dmesg 2>/dev/null | grep 'BLOCKED:' | \
194
- sed -n 's/.*DST=\([^ ]*\).*DPT=\([^ ]*\).*/\1\t\2/p' | \
195
- sort -u > /tmp/blocked.txt
196
- else
197
- touch /tmp/blocked.txt
198
- fi
199
-
200
- # ── 11. Build JSON with jq ──
201
- echo "[SANDBOX] Building report..." >&2
202
-
203
- # Ensure all temp files exist
204
- touch /tmp/fs-created.txt /tmp/fs-deleted.txt /tmp/dns-queries.txt \
205
- /tmp/sensitive-read.txt /tmp/sensitive-written.txt \
206
- /tmp/connections.txt /tmp/suspicious-cmds.txt /tmp/install.log \
207
- /tmp/dns-resolutions.txt /tmp/http-requests.txt /tmp/http-bodies.txt \
208
- /tmp/tls-connections.txt /tmp/blocked.txt
209
-
210
- INSTALL_OUTPUT=$(head -c 5000 /tmp/install.log)
211
-
212
- FS_CREATED=$(jq -R -s 'split("\n") | map(select(length > 0))' < /tmp/fs-created.txt)
213
- FS_DELETED=$(jq -R -s 'split("\n") | map(select(length > 0))' < /tmp/fs-deleted.txt)
214
- DNS=$(jq -R -s 'split("\n") | map(select(length > 0))' < /tmp/dns-queries.txt)
215
- SENS_READ=$(jq -R -s 'split("\n") | map(select(length > 0))' < /tmp/sensitive-read.txt)
216
- SENS_WRITTEN=$(jq -R -s 'split("\n") | map(select(length > 0))' < /tmp/sensitive-written.txt)
217
-
218
- CONNS=$(jq -R -s 'split("\n") | map(select(length > 0)) | map(
219
- split("\t") | {host: .[0], port: (.[1] | tonumber), protocol: "TCP"}
220
- )' < /tmp/connections.txt)
221
-
222
- PROCS=$(jq -R -s 'split("\n") | map(select(length > 0)) | map(
223
- split("\t") | {command: .[1], pid: (.[0] | tonumber)}
224
- )' < /tmp/suspicious-cmds.txt)
225
-
226
- DNS_RESOLUTIONS=$(jq -R -s 'split("\n") | map(select(length > 0)) | map(
227
- split("\t") | {domain: .[0], ip: .[1]}
228
- )' < /tmp/dns-resolutions.txt)
229
-
230
- HTTP_REQUESTS=$(jq -R -s 'split("\n") | map(select(length > 0)) | map(
231
- split("\t") | {method: .[0], host: .[1], path: .[2]}
232
- )' < /tmp/http-requests.txt)
233
-
234
- HTTP_BODIES=$(jq -R -s 'split("\n") | map(select(length > 0))' < /tmp/http-bodies.txt)
235
-
236
- TLS_CONNS=$(jq -R -s 'split("\n") | map(select(length > 0)) | map(
237
- split("\t") | {domain: .[0], ip: .[1], port: (.[2] | tonumber)}
238
- )' < /tmp/tls-connections.txt)
239
-
240
- BLOCKED=$(jq -R -s 'split("\n") | map(select(length > 0)) | map(
241
- split("\t") | {ip: .[0], port: (.[1] | tonumber)}
242
- )' < /tmp/blocked.txt)
243
-
244
- # ── Final JSON ──
245
- jq -n \
246
- --arg package "$PACKAGE" \
247
- --arg timestamp "$TIMESTAMP" \
248
- --arg mode "$MODE" \
249
- --argjson duration "${DURATION_MS:-0}" \
250
- --argjson fs_created "$FS_CREATED" \
251
- --argjson fs_deleted "$FS_DELETED" \
252
- --argjson dns "$DNS" \
253
- --argjson connections "$CONNS" \
254
- --argjson processes "$PROCS" \
255
- --argjson sensitive_read "$SENS_READ" \
256
- --argjson sensitive_written "$SENS_WRITTEN" \
257
- --argjson dns_resolutions "$DNS_RESOLUTIONS" \
258
- --argjson http_requests "$HTTP_REQUESTS" \
259
- --argjson http_bodies "$HTTP_BODIES" \
260
- --argjson tls_connections "$TLS_CONNS" \
261
- --argjson blocked_connections "$BLOCKED" \
262
- --arg install_output "$INSTALL_OUTPUT" \
263
- --argjson exit_code "${EXIT_CODE:-1}" \
264
- '{
265
- package: $package,
266
- timestamp: $timestamp,
267
- mode: $mode,
268
- duration_ms: $duration,
269
- filesystem: {
270
- created: $fs_created,
271
- deleted: $fs_deleted,
272
- modified: []
273
- },
274
- network: {
275
- dns_queries: $dns,
276
- dns_resolutions: $dns_resolutions,
277
- http_connections: $connections,
278
- http_requests: $http_requests,
279
- http_bodies: $http_bodies,
280
- tls_connections: $tls_connections,
281
- blocked_connections: $blocked_connections
282
- },
283
- processes: {
284
- spawned: $processes
285
- },
286
- sensitive_files: {
287
- read: $sensitive_read,
288
- written: $sensitive_written
289
- },
290
- install_output: $install_output,
291
- exit_code: $exit_code
292
- }'
1
+ #!/bin/sh
2
+ PACKAGE="$1"
3
+ MODE="${2:-permissive}"
4
+
5
+ if [ -z "$PACKAGE" ]; then
6
+ echo "Usage: sandbox-runner.sh <package-name> [permissive|strict]" >&2
7
+ exit 1
8
+ fi
9
+
10
+ TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
11
+ START_MS=$(date +%s%3N 2>/dev/null || echo 0)
12
+
13
+ # ══════════════════════════════════════════════════════════════
14
+ # PHASE 1: Root-privileged setup (iptables, tcpdump, filesystem snapshot)
15
+ # Runs as root to access raw sockets and kernel netfilter.
16
+ # ══════════════════════════════════════════════════════════════
17
+
18
+ # ── 0. Strict mode: iptables rules (requires root + NET_ADMIN) ──
19
+ if [ "$MODE" = "strict" ]; then
20
+ echo "[SANDBOX] STRICT MODE blocking non-essential network..." >&2
21
+ # Allow loopback
22
+ iptables -A OUTPUT -o lo -j ACCEPT
23
+ # Allow DNS (UDP 53)
24
+ iptables -A OUTPUT -p udp --dport 53 -j ACCEPT
25
+ # Allow registry.npmjs.org (resolve + allow)
26
+ REGISTRY_IPS=$(nslookup registry.npmjs.org 2>/dev/null | grep -E '^Address:' | tail -n+2 | awk '{print $2}')
27
+ for ip in $REGISTRY_IPS; do
28
+ iptables -A OUTPUT -d "$ip" -p tcp --dport 443 -j ACCEPT
29
+ done
30
+ # Log + reject everything else
31
+ iptables -A OUTPUT -j LOG --log-prefix "BLOCKED: "
32
+ iptables -A OUTPUT -j REJECT
33
+ echo "[SANDBOX] iptables rules applied." >&2
34
+ fi
35
+
36
+ # ── 1. Filesystem snapshot BEFORE install ──
37
+ echo "[SANDBOX] Snapshot filesystem before install..." >&2
38
+ find / -type f 2>/dev/null | sort > /tmp/fs-before.txt
39
+
40
+ # ── 2. tcpdump: separate captures for DNS, HTTP, TLS (requires root + NET_RAW) ──
41
+ echo "[SANDBOX] Starting network capture..." >&2
42
+ tcpdump -i any -nn 'port 53' -l > /tmp/dns.log 2>/dev/null &
43
+ DNS_PID=$!
44
+ tcpdump -i any -nn -A 'tcp port 80' -l > /tmp/http.log 2>/dev/null &
45
+ HTTP_PID=$!
46
+ tcpdump -i any -nn 'tcp port 443' -l > /tmp/tls.log 2>/dev/null &
47
+ TLS_PID=$!
48
+ tcpdump -i any -nn 'not port 53 and not port 80 and not port 443' -l > /tmp/other.log 2>/dev/null &
49
+ OTHER_PID=$!
50
+ sleep 1
51
+
52
+ # ══════════════════════════════════════════════════════════════
53
+ # PHASE 2: Unprivileged install (su sandboxuser)
54
+ # npm install runs as sandboxuser with strace for syscall tracing.
55
+ # strace can trace child processes without SYS_PTRACE (parent→child).
56
+ # ══════════════════════════════════════════════════════════════
57
+
58
+ # ── 2b. CI environment simulation ──
59
+ # Simulate CI to trigger CI-aware malware that checks for these env vars
60
+ echo "[SANDBOX] Simulating CI environment..." >&2
61
+ export CI=true
62
+ export GITHUB_ACTIONS=true
63
+ export GITLAB_CI=true
64
+ export TRAVIS=true
65
+ export CIRCLECI=true
66
+ export JENKINS_URL=http://localhost:8080
67
+
68
+ # ── 2c. Canary tokens (honeypots) ──
69
+ # Use Docker-injected dynamic tokens if available, otherwise static fallbacks.
70
+ # If exfiltrated via network/DNS/files, sandbox.js detects the theft.
71
+ export GITHUB_TOKEN="${GITHUB_TOKEN:-MUADDIB_CANARY_GITHUB_f8k3t0k3n}"
72
+ export NPM_TOKEN="${NPM_TOKEN:-MUADDIB_CANARY_NPM_s3cr3tt0k3n}"
73
+ export AWS_ACCESS_KEY_ID="${AWS_ACCESS_KEY_ID:-MUADDIB_CANARY_AKIAIOSFODNN7EXAMPLE}"
74
+ export AWS_SECRET_ACCESS_KEY="${AWS_SECRET_ACCESS_KEY:-MUADDIB_CANARY_wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY}"
75
+ export SLACK_WEBHOOK_URL="${SLACK_WEBHOOK_URL:-https://hooks.slack.com/MUADDIB_CANARY_SLACK}"
76
+ export DISCORD_WEBHOOK_URL="${DISCORD_WEBHOOK_URL:-https://discord.com/api/webhooks/MUADDIB_CANARY_DISCORD}"
77
+
78
+ # ── 3. npm install with strace — as sandboxuser ──
79
+ echo "[SANDBOX] Installing $PACKAGE as sandboxuser..." >&2
80
+ cd /sandbox/install
81
+ # Ensure sandboxuser owns the install directory
82
+ chown sandboxuser:sandboxuser /sandbox/install
83
+ # Run npm install as sandboxuser via su, wrapped in strace for syscall tracing
84
+ su sandboxuser -s /bin/sh -c "
85
+ strace -f -e trace=network,process,open,openat,connect,execve,sendto,recvfrom \
86
+ -o /tmp/strace.log \
87
+ npm install \"$PACKAGE\" --ignore-scripts=false > /tmp/install.log 2>&1
88
+ "
89
+ EXIT_CODE=$?
90
+
91
+ # ══════════════════════════════════════════════════════════════
92
+ # PHASE 3: Post-install analysis (back as root for full access)
93
+ # ══════════════════════════════════════════════════════════════
94
+
95
+ # ── 4. Filesystem snapshot AFTER install ──
96
+ echo "[SANDBOX] Snapshot filesystem after install..." >&2
97
+ find / -type f 2>/dev/null | sort > /tmp/fs-after.txt
98
+
99
+ # Stop tcpdump
100
+ kill "$DNS_PID" "$HTTP_PID" "$TLS_PID" "$OTHER_PID" 2>/dev/null
101
+ wait "$DNS_PID" "$HTTP_PID" "$TLS_PID" "$OTHER_PID" 2>/dev/null
102
+
103
+ END_MS=$(date +%s%3N 2>/dev/null || echo 0)
104
+ DURATION_MS=$((END_MS - START_MS))
105
+ [ "$DURATION_MS" -lt 0 ] 2>/dev/null && DURATION_MS=0
106
+
107
+ # ── 5. Filesystem diff ──
108
+ echo "[SANDBOX] Analyzing filesystem changes..." >&2
109
+ comm -13 /tmp/fs-before.txt /tmp/fs-after.txt | grep -v '^/sandbox/install/' | grep -v '^/tmp/' > /tmp/fs-created.txt
110
+ comm -23 /tmp/fs-before.txt /tmp/fs-after.txt | grep -v '^/sandbox/install/' > /tmp/fs-deleted.txt
111
+
112
+ # ── 6. Parse strace ──
113
+ echo "[SANDBOX] Parsing strace..." >&2
114
+
115
+ SENSITIVE='\.npmrc|\.ssh/|\.aws/|\.env|/etc/passwd|/etc/shadow|\.gitconfig|\.bash_history'
116
+
117
+ # 6a. Sensitive file access (read)
118
+ grep -E 'openat\(' /tmp/strace.log 2>/dev/null | \
119
+ grep -E "$SENSITIVE" | \
120
+ grep 'O_RDONLY' | \
121
+ sed 's/.*openat([^,]*, "\([^"]*\)".*/\1/' | \
122
+ sort -u > /tmp/sensitive-read.txt
123
+
124
+ # 6b. Sensitive file access (write)
125
+ grep -E 'openat\(' /tmp/strace.log 2>/dev/null | \
126
+ grep -E "$SENSITIVE" | \
127
+ grep -E 'O_WRONLY|O_RDWR|O_CREAT' | \
128
+ sed 's/.*openat([^,]*, "\([^"]*\)".*/\1/' | \
129
+ sort -u > /tmp/sensitive-written.txt
130
+
131
+ # 6c. Suspicious execve
132
+ grep 'execve(' /tmp/strace.log 2>/dev/null | \
133
+ grep '= 0' | \
134
+ grep -vE 'execve\("[^"]*/(node|npm|npx|sh|git)"' | \
135
+ sed -n 's/.*\[pid \([0-9]*\)\].*execve("\([^"]*\)".*/\1\t\2/p' > /tmp/suspicious-cmds.txt
136
+
137
+ grep 'execve(' /tmp/strace.log 2>/dev/null | \
138
+ grep '= 0' | \
139
+ grep -vE 'execve\("[^"]*/(node|npm|npx|sh|git)"' | \
140
+ grep -v '\[pid' | \
141
+ sed -n 's/.*execve("\([^"]*\)".*/0\t\1/p' >> /tmp/suspicious-cmds.txt
142
+
143
+ # 6d. Outgoing connections (AF_INET, successful)
144
+ grep 'connect(' /tmp/strace.log 2>/dev/null | \
145
+ grep 'AF_INET' | grep -v 'AF_INET6' | \
146
+ grep '= 0' | \
147
+ sed -n 's/.*sin_port=htons(\([0-9]*\)).*sin_addr=inet_addr("\([^"]*\)").*/\2\t\1/p' | \
148
+ grep -v ' 65535$' | \
149
+ grep -v '^127\.' | \
150
+ sort -u > /tmp/connections.txt
151
+
152
+ # ── 7. Parse DNS resolutions (query → answer pairs) ──
153
+ echo "[SANDBOX] Parsing DNS resolutions..." >&2
154
+
155
+ # Extract DNS queries and answers from dns.log
156
+ # Format: "domain query_type answer_ip"
157
+ awk '
158
+ /A\?/ { domain=$0; sub(/.*A\? /,"",domain); sub(/ .*$/,"",domain); gsub(/\.$/,"",domain); pending=domain }
159
+ /A [0-9]+\.[0-9]+\.[0-9]+\.[0-9]+/ && pending {
160
+ ip=$0; sub(/.*A /,"",ip); sub(/ .*$/,"",ip);
161
+ print pending "\t" ip;
162
+ pending=""
163
+ }
164
+ ' /tmp/dns.log 2>/dev/null | sort -u > /tmp/dns-resolutions.txt
165
+
166
+ # Plain DNS query list (for backward compat)
167
+ grep -oE '(A|AAAA)\? [^ ]+' /tmp/dns.log 2>/dev/null | \
168
+ awk '{print $2}' | sed 's/\.$//' | sort -u > /tmp/dns-queries.txt
169
+
170
+ # ── 8. Parse HTTP requests ──
171
+ echo "[SANDBOX] Parsing HTTP requests..." >&2
172
+
173
+ # Extract HTTP method, host, path from http.log
174
+ awk '
175
+ /^[0-9].*length [0-9]/ { next }
176
+ /(GET|POST|PUT|DELETE|PATCH) \// {
177
+ method=$0; sub(/.*((GET|POST|PUT|DELETE|PATCH)) /,"",method);
178
+ path=method; sub(/ .*$/,"",path);
179
+ meth=$0; match(meth,/(GET|POST|PUT|DELETE|PATCH)/); meth=substr(meth,RSTART,RLENGTH);
180
+ pending_method=meth; pending_path=path; pending_host=""; pending_body=""
181
+ }
182
+ /^Host:/ && pending_method { pending_host=$2; gsub(/\r/,"",pending_host) }
183
+ /^\r?$/ && pending_method && pending_host {
184
+ print pending_method "\t" pending_host "\t" pending_path;
185
+ pending_method=""; pending_host=""; pending_path=""
186
+ }
187
+ ' /tmp/http.log 2>/dev/null > /tmp/http-requests.txt
188
+
189
+ # Extract HTTP body snippets for exfiltration detection
190
+ awk '
191
+ /^(POST|PUT|PATCH) / { capturing=1; body="" }
192
+ capturing && /^[^\t]/ && !/^(GET|POST|PUT|DELETE|PATCH|HTTP|Host:|Content|Accept|User-Agent|Connection)/ {
193
+ if (length(body) < 500) body = body $0
194
+ }
195
+ /^\r?$/ && capturing { if (length(body)>0) print body; capturing=0; body="" }
196
+ ' /tmp/http.log 2>/dev/null > /tmp/http-bodies.txt
197
+
198
+ # ── 9. Parse TLS connections (SNI via IP correlation) ──
199
+ echo "[SANDBOX] Parsing TLS connections..." >&2
200
+
201
+ # Map IPs to domains from DNS resolutions, then correlate with TLS IPs
202
+ awk '{print $1}' /tmp/connections.txt 2>/dev/null | sort -u > /tmp/tls-ips.txt
203
+
204
+ # Build domain→IP map, then find TLS connections
205
+ awk -F'\t' '
206
+ NR==FNR { ip_domain[$2]=$1; next }
207
+ { if ($1 in ip_domain) print ip_domain[$1] "\t" $1 "\t" $2 }
208
+ ' /tmp/dns-resolutions.txt /tmp/connections.txt 2>/dev/null | \
209
+ grep ' 443$' | sort -u > /tmp/tls-connections.txt
210
+
211
+ # ── 10. Blocked connections (strict mode — requires root for dmesg) ──
212
+ if [ "$MODE" = "strict" ]; then
213
+ dmesg 2>/dev/null | grep 'BLOCKED:' | \
214
+ sed -n 's/.*DST=\([^ ]*\).*DPT=\([^ ]*\).*/\1\t\2/p' | \
215
+ sort -u > /tmp/blocked.txt
216
+ else
217
+ touch /tmp/blocked.txt
218
+ fi
219
+
220
+ # ── 11. Build JSON with jq ──
221
+ echo "[SANDBOX] Building report..." >&2
222
+
223
+ # Ensure all temp files exist
224
+ touch /tmp/fs-created.txt /tmp/fs-deleted.txt /tmp/dns-queries.txt \
225
+ /tmp/sensitive-read.txt /tmp/sensitive-written.txt \
226
+ /tmp/connections.txt /tmp/suspicious-cmds.txt /tmp/install.log \
227
+ /tmp/dns-resolutions.txt /tmp/http-requests.txt /tmp/http-bodies.txt \
228
+ /tmp/tls-connections.txt /tmp/blocked.txt
229
+
230
+ INSTALL_OUTPUT=$(head -c 5000 /tmp/install.log)
231
+
232
+ FS_CREATED=$(jq -R -s 'split("\n") | map(select(length > 0))' < /tmp/fs-created.txt)
233
+ FS_DELETED=$(jq -R -s 'split("\n") | map(select(length > 0))' < /tmp/fs-deleted.txt)
234
+ DNS=$(jq -R -s 'split("\n") | map(select(length > 0))' < /tmp/dns-queries.txt)
235
+ SENS_READ=$(jq -R -s 'split("\n") | map(select(length > 0))' < /tmp/sensitive-read.txt)
236
+ SENS_WRITTEN=$(jq -R -s 'split("\n") | map(select(length > 0))' < /tmp/sensitive-written.txt)
237
+
238
+ CONNS=$(jq -R -s 'split("\n") | map(select(length > 0)) | map(
239
+ split("\t") | {host: .[0], port: (.[1] | tonumber), protocol: "TCP"}
240
+ )' < /tmp/connections.txt)
241
+
242
+ PROCS=$(jq -R -s 'split("\n") | map(select(length > 0)) | map(
243
+ split("\t") | {command: .[1], pid: (.[0] | tonumber)}
244
+ )' < /tmp/suspicious-cmds.txt)
245
+
246
+ DNS_RESOLUTIONS=$(jq -R -s 'split("\n") | map(select(length > 0)) | map(
247
+ split("\t") | {domain: .[0], ip: .[1]}
248
+ )' < /tmp/dns-resolutions.txt)
249
+
250
+ HTTP_REQUESTS=$(jq -R -s 'split("\n") | map(select(length > 0)) | map(
251
+ split("\t") | {method: .[0], host: .[1], path: .[2]}
252
+ )' < /tmp/http-requests.txt)
253
+
254
+ HTTP_BODIES=$(jq -R -s 'split("\n") | map(select(length > 0))' < /tmp/http-bodies.txt)
255
+
256
+ TLS_CONNS=$(jq -R -s 'split("\n") | map(select(length > 0)) | map(
257
+ split("\t") | {domain: .[0], ip: .[1], port: (.[2] | tonumber)}
258
+ )' < /tmp/tls-connections.txt)
259
+
260
+ BLOCKED=$(jq -R -s 'split("\n") | map(select(length > 0)) | map(
261
+ split("\t") | {ip: .[0], port: (.[1] | tonumber)}
262
+ )' < /tmp/blocked.txt)
263
+
264
+ # ── Final JSON (prefixed with delimiter for reliable parsing) ──
265
+ echo "---MUADDIB-REPORT-START---"
266
+ jq -n \
267
+ --arg package "$PACKAGE" \
268
+ --arg timestamp "$TIMESTAMP" \
269
+ --arg mode "$MODE" \
270
+ --argjson duration "${DURATION_MS:-0}" \
271
+ --argjson fs_created "$FS_CREATED" \
272
+ --argjson fs_deleted "$FS_DELETED" \
273
+ --argjson dns "$DNS" \
274
+ --argjson connections "$CONNS" \
275
+ --argjson processes "$PROCS" \
276
+ --argjson sensitive_read "$SENS_READ" \
277
+ --argjson sensitive_written "$SENS_WRITTEN" \
278
+ --argjson dns_resolutions "$DNS_RESOLUTIONS" \
279
+ --argjson http_requests "$HTTP_REQUESTS" \
280
+ --argjson http_bodies "$HTTP_BODIES" \
281
+ --argjson tls_connections "$TLS_CONNS" \
282
+ --argjson blocked_connections "$BLOCKED" \
283
+ --arg install_output "$INSTALL_OUTPUT" \
284
+ --argjson exit_code "${EXIT_CODE:-1}" \
285
+ '{
286
+ package: $package,
287
+ timestamp: $timestamp,
288
+ mode: $mode,
289
+ duration_ms: $duration,
290
+ filesystem: {
291
+ created: $fs_created,
292
+ deleted: $fs_deleted,
293
+ modified: []
294
+ },
295
+ network: {
296
+ dns_queries: $dns,
297
+ dns_resolutions: $dns_resolutions,
298
+ http_connections: $connections,
299
+ http_requests: $http_requests,
300
+ http_bodies: $http_bodies,
301
+ tls_connections: $tls_connections,
302
+ blocked_connections: $blocked_connections
303
+ },
304
+ processes: {
305
+ spawned: $processes
306
+ },
307
+ sensitive_files: {
308
+ read: $sensitive_read,
309
+ written: $sensitive_written
310
+ },
311
+ install_output: $install_output,
312
+ exit_code: $exit_code
313
+ }'