108 lines
3.5 KiB
Bash
Executable File
108 lines
3.5 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
# Layer 3: Network egress test — monitors outbound connections during MCP session.
|
|
#
|
|
# Runs a full MCP session (initialize → index → search → EOF) under strace
|
|
# and verifies only expected connections are made.
|
|
#
|
|
# Linux only (strace required). macOS/Windows: skip with success.
|
|
#
|
|
# Usage: scripts/security-network.sh <binary-path>
|
|
|
|
BINARY="${1:?usage: security-network.sh <binary-path>}"
|
|
|
|
if [[ ! -f "$BINARY" ]]; then
|
|
echo "FAIL: binary not found: $BINARY"
|
|
exit 1
|
|
fi
|
|
|
|
echo "=== Layer 3: Network Egress Test ==="
|
|
|
|
# Skip on non-Linux (no strace)
|
|
if [[ "$(uname)" != "Linux" ]]; then
|
|
echo "SKIP: strace not available on $(uname) — covered by binary string audit"
|
|
exit 0
|
|
fi
|
|
|
|
if ! command -v strace &>/dev/null; then
|
|
echo "SKIP: strace not installed"
|
|
exit 0
|
|
fi
|
|
|
|
TMPDIR=$(mktemp -d)
|
|
trap 'rm -rf "$TMPDIR"' EXIT
|
|
|
|
# Create a minimal test project
|
|
mkdir -p "$TMPDIR/project/src"
|
|
cat > "$TMPDIR/project/src/main.py" << 'EOF'
|
|
def main():
|
|
print("hello")
|
|
EOF
|
|
|
|
# MCP session input (jsonrpc over stdio)
|
|
cat > "$TMPDIR/input.jsonl" << JSONL
|
|
{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"test","version":"1.0"}}}
|
|
{"jsonrpc":"2.0","method":"notifications/initialized"}
|
|
{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"index_repository","arguments":{"repo_path":"$TMPDIR/project"}}}
|
|
{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"search_graph","arguments":{"name_pattern":"main"}}}
|
|
JSONL
|
|
|
|
STRACE_LOG="$TMPDIR/strace.log"
|
|
|
|
echo "Running MCP session under strace..."
|
|
|
|
# Run binary with strace monitoring connect() syscalls
|
|
# -f: follow forks, -e trace=connect: only log connect() calls
|
|
timeout 30 strace -f -e trace=connect \
|
|
"$BINARY" < "$TMPDIR/input.jsonl" \
|
|
> "$TMPDIR/output.jsonl" 2> "$STRACE_LOG" || true
|
|
|
|
echo ""
|
|
echo "--- Connection log ---"
|
|
|
|
FAIL=0
|
|
|
|
# Parse strace output for connect() calls with AF_INET (IPv4 network)
|
|
# Format: connect(fd, {sa_family=AF_INET, sin_port=htons(443), sin_addr=inet_addr("x.x.x.x")}, ...)
|
|
if grep 'sa_family=AF_INET' "$STRACE_LOG" > "$TMPDIR/connections.log" 2>/dev/null; then
|
|
while IFS= read -r conn; do
|
|
# Extract destination IP and port
|
|
ip=$(echo "$conn" | grep -oP 'inet_addr\("\K[^"]+' || echo "unknown")
|
|
port=$(echo "$conn" | grep -oP 'htons\(\K[0-9]+' || echo "0")
|
|
|
|
# Allowed connections:
|
|
# - 127.0.0.1 (localhost, any port)
|
|
# - DNS (port 53, any IP)
|
|
# - api.github.com (140.82.x.x range, port 443) — update check
|
|
case "$ip" in
|
|
127.0.0.1|0.0.0.0)
|
|
echo " OK: localhost:$port"
|
|
;;
|
|
*)
|
|
if [[ "$port" == "53" ]]; then
|
|
echo " OK: DNS lookup to $ip"
|
|
elif [[ "$port" == "443" ]]; then
|
|
echo " REVIEW: HTTPS to $ip:$port (expected: api.github.com for update check)"
|
|
# This is expected — the binary checks for updates on startup
|
|
else
|
|
echo " BLOCKED: Unexpected connection to $ip:$port"
|
|
FAIL=1
|
|
fi
|
|
;;
|
|
esac
|
|
done < "$TMPDIR/connections.log"
|
|
else
|
|
echo " No outbound connections detected."
|
|
fi
|
|
|
|
echo ""
|
|
if [[ $FAIL -ne 0 ]]; then
|
|
echo "=== NETWORK EGRESS TEST FAILED ==="
|
|
echo "Unexpected outbound connections detected. Full strace log:"
|
|
grep 'connect(' "$STRACE_LOG" || true
|
|
exit 1
|
|
fi
|
|
|
|
echo "=== Network egress test passed ==="
|