Files
deusdata--codebase-memory-mcp/scripts/security-ui.sh
Martin Vogel f7315b9bc5 Robust UI audit: skip inline URL scan on dist/, keep structural checks
Minified JS in dist/ contains hundreds of string-constant URLs from
bundled npm libraries (React error pages, W3C namespace URIs, CDN
references, OSS credits) that are never fetched at runtime. Scanning
them produced an endless stream of false positives requiring per-URL
allowlisting.

Fix: split the A1 URL check by directory:
- src/ (our code): strict — any external URL is blocked
- dist/ (npm build output): skip inline URL scan entirely

Structural checks (A2-A6) still apply to dist/:
- External <script>/<link> loads in HTML
- Tracking/analytics identifiers
- Iframes, eval/Function, external WebSockets
2026-03-20 20:56:52 +01:00

205 lines
8.4 KiB
Bash
Executable File

#!/usr/bin/env bash
set -euo pipefail
# Layer 6: Graph UI security audit.
#
# Audits:
# A. Frontend asset scan (embedded JS/CSS/HTML or graph-ui/dist/)
# B. HTTP server binding (must be 127.0.0.1 only)
# C. RPC proxy scope (no system()/popen() in HTTP handler path)
# D. CORS check (no wildcard Access-Control-Allow-Origin)
#
# Usage: scripts/security-ui.sh
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
FAIL=0
# Use mktemp for all temp files (cross-platform safe)
SEC_TMPDIR=$(mktemp -d)
trap 'rm -rf "$SEC_TMPDIR"' EXIT
echo "=== Layer 6: Graph UI Security Audit ==="
# ── A. Frontend asset scan ───────────────────────────────────────
echo ""
echo "--- A. Frontend asset scan ---"
# Check both built dist and embedded asset source
UI_DIRS=()
[[ -d "$ROOT/graph-ui/dist" ]] && UI_DIRS+=("$ROOT/graph-ui/dist")
[[ -d "$ROOT/graph-ui/src" ]] && UI_DIRS+=("$ROOT/graph-ui/src")
if [[ ${#UI_DIRS[@]} -eq 0 ]]; then
echo "SKIP: No graph-ui directory found."
else
for UI_DIR in "${UI_DIRS[@]}"; do
echo "Scanning: $UI_DIR"
# A1: No external domains in JS/CSS/TS source code.
# For src/ (our code): strict — any external URL is blocked.
# For dist/ (bundled npm output): skip inline URL scan — minified JS
# contains hundreds of string-constant URLs from libraries (React error
# pages, W3C namespace URIs, CDN references, OSS credits) that are never
# fetched at runtime. Scanning them is noise. Structural checks (A2-A6)
# still apply to dist/.
is_dist=false
[[ "$UI_DIR" == *"/dist" || "$UI_DIR" == *"/dist/" ]] && is_dist=true
if ! $is_dist; then
echo " Checking for external domains (source)..."
if find "$UI_DIR" -type f \( -name '*.js' -o -name '*.ts' -o -name '*.tsx' -o -name '*.css' \) -exec grep -lE 'https?://' {} \; 2>/dev/null | head -20 > "$SEC_TMPDIR/urls"; then
while IFS= read -r file; do
relfile="${file#"$ROOT/"}"
grep -onE 'https?://[^\s"'"'"')]+' "$file" 2>/dev/null | while IFS=: read -r lineno url; do
case "$url" in
http://localhost*|http://127.0.0.1*|https://localhost*|https://127.0.0.1*)
;; # OK — local dev/runtime
*)
echo " BLOCKED: ${relfile}:${lineno}: External URL: $url"
touch "$SEC_TMPDIR/fail_flag"
;;
esac
done
done < "$SEC_TMPDIR/urls"
fi
[[ -f "$SEC_TMPDIR/fail_flag" ]] && FAIL=1 && rm -f "$SEC_TMPDIR/fail_flag"
else
echo " Skipping inline URL scan for dist/ (bundled library strings)."
echo " Structural checks (script loads, tracking, eval, iframes) still apply."
fi
# A2: No external script/link loads in HTML
echo " Checking for external script/link loads..."
if find "$UI_DIR" -type f -name '*.html' -exec grep -lE '<script\s+src=|<link\s+href=' {} \; 2>/dev/null > "$SEC_TMPDIR/scripts"; then
while IFS= read -r file; do
relfile="${file#"$ROOT/"}"
if grep -nE '<script\s+src="https?://|<link\s+href="https?://' "$file" 2>/dev/null \
| grep -v 'localhost' | grep -v '127.0.0.1' \
| grep -v 'fonts.googleapis.com' | grep -v 'fonts.gstatic.com'; then
echo " BLOCKED: ${relfile}: External script/link load detected"
FAIL=1
fi
done < "$SEC_TMPDIR/scripts"
fi
# A3: No tracking/analytics
echo " Checking for tracking/analytics..."
TRACKING='google-analytics|gtag|mixpanel|segment\.com|hotjar|sentry\.io|plausible|posthog'
if find "$UI_DIR" -type f \( -name '*.js' -o -name '*.ts' -o -name '*.tsx' -o -name '*.html' \) \
-exec grep -lE "$TRACKING" {} \; 2>/dev/null > "$SEC_TMPDIR/track"; then
while IFS= read -r file; do
relfile="${file#"$ROOT/"}"
echo " BLOCKED: ${relfile}: Tracking/analytics reference found"
grep -nE "$TRACKING" "$file" | head -3
FAIL=1
done < "$SEC_TMPDIR/track"
fi
# A4: No hidden iframes
echo " Checking for iframes..."
if find "$UI_DIR" -type f -name '*.html' -exec grep -li '<iframe' {} \; 2>/dev/null > "$SEC_TMPDIR/iframe"; then
while IFS= read -r file; do
relfile="${file#"$ROOT/"}"
echo " BLOCKED: ${relfile}: iframe detected"
FAIL=1
done < "$SEC_TMPDIR/iframe"
fi
# A5: No eval/Function constructor in JS
echo " Checking for eval/Function constructor..."
if find "$UI_DIR" -type f \( -name '*.js' -o -name '*.ts' -o -name '*.tsx' \) \
-exec grep -nE '\beval\s*\(|new\s+Function\s*\(' {} \; 2>/dev/null | grep -v node_modules | grep -v '\.test\.' > "$SEC_TMPDIR/eval"; then
while IFS= read -r match; do
echo " REVIEW: eval/Function found: $match"
done < "$SEC_TMPDIR/eval"
fi
# A6: No WebSocket to external
echo " Checking for external WebSocket connections..."
if find "$UI_DIR" -type f \( -name '*.js' -o -name '*.ts' -o -name '*.tsx' \) \
-exec grep -nE 'wss?://' {} \; 2>/dev/null | grep -v 'localhost' | grep -v '127.0.0.1' > "$SEC_TMPDIR/ws"; then
while IFS= read -r match; do
echo " BLOCKED: External WebSocket: $match"
FAIL=1
done < "$SEC_TMPDIR/ws"
fi
done
fi
# ── B. HTTP server binding check ─────────────────────────────────
echo ""
echo "--- B. HTTP server binding check ---"
HTTP_SERVER="$ROOT/src/ui/http_server.c"
if [[ -f "$HTTP_SERVER" ]]; then
# Must bind to 127.0.0.1 only
if grep -q '127\.0\.0\.1' "$HTTP_SERVER"; then
echo "OK: Server binds to 127.0.0.1"
else
echo "BLOCKED: No 127.0.0.1 binding found in http_server.c"
FAIL=1
fi
# Must NOT bind to 0.0.0.0 or INADDR_ANY
if grep -E '0\.0\.0\.0|INADDR_ANY|in6addr_any' "$HTTP_SERVER" | grep -v '^\s*//' | grep -v '^\s*\*' > /dev/null 2>&1; then
echo "BLOCKED: Server may bind to all interfaces (0.0.0.0/INADDR_ANY found)"
FAIL=1
else
echo "OK: No 0.0.0.0/INADDR_ANY binding"
fi
else
echo "SKIP: http_server.c not found"
fi
# ── C. RPC proxy scope check ─────────────────────────────────────
echo ""
echo "--- C. RPC proxy scope check ---"
if [[ -f "$HTTP_SERVER" ]]; then
# The HTTP handler should not directly call system()/popen()
# (fork/execl for indexing is allowed as it's tracked)
if grep -n 'system(' "$HTTP_SERVER" | grep -v '^\s*//' | grep -v '^\s*\*' > /dev/null 2>&1; then
echo "BLOCKED: system() call found in HTTP server (use subprocess instead)"
FAIL=1
else
echo "OK: No system() calls in HTTP handler"
fi
else
echo "SKIP: http_server.c not found"
fi
# ── D. CORS check ────────────────────────────────────────────────
echo ""
echo "--- D. CORS check ---"
if [[ -f "$HTTP_SERVER" ]]; then
if grep -E 'Allow-Origin:\s*\*' "$HTTP_SERVER" | grep -v '^\s*//' | grep -v '^\s*\*' > /dev/null 2>&1; then
echo "BLOCKED: CORS wildcard (Access-Control-Allow-Origin: *) found"
echo " This allows any website to access the local server."
FAIL=1
else
echo "OK: No CORS wildcard found"
fi
# Check that CORS reflects localhost origins
if grep -q 'localhost' "$HTTP_SERVER" && grep -q 'update_cors\|Access-Control-Allow-Origin' "$HTTP_SERVER"; then
echo "OK: CORS appears to validate localhost origins"
fi
else
echo "SKIP: http_server.c not found"
fi
# ── Summary ──────────────────────────────────────────────────────
echo ""
if [[ $FAIL -ne 0 ]]; then
echo "=== UI SECURITY AUDIT FAILED ==="
exit 1
fi
echo "=== UI security audit passed ==="