#!/usr/bin/env bash # gstack-security-dashboard — community prompt-injection attack stats # # Reads the `security` section of the community-pulse edge function response # (supabase/functions/community-pulse/index.ts). Shows aggregated attack # data across all gstack users on telemetry=community. # # Call signature: # gstack-security-dashboard # human-readable dashboard # gstack-security-dashboard --json # machine-readable (CI / scripts) # # Env overrides (for testing): # GSTACK_DIR — override auto-detected gstack root # GSTACK_SUPABASE_URL — override Supabase project URL # GSTACK_SUPABASE_ANON_KEY — override Supabase anon key set -uo pipefail GSTACK_DIR="${GSTACK_DIR:-$(cd "$(dirname "$0")/.." && pwd)}" # Source Supabase config if [ -z "${GSTACK_SUPABASE_URL:-}" ] && [ -f "$GSTACK_DIR/supabase/config.sh" ]; then . "$GSTACK_DIR/supabase/config.sh" fi SUPABASE_URL="${GSTACK_SUPABASE_URL:-}" ANON_KEY="${GSTACK_SUPABASE_ANON_KEY:-}" JSON_MODE=0 [ "${1:-}" = "--json" ] && JSON_MODE=1 if [ -z "$SUPABASE_URL" ] || [ -z "$ANON_KEY" ]; then if [ "$JSON_MODE" = "1" ]; then echo '{"error":"supabase_not_configured"}' exit 0 fi echo "gstack security dashboard" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo "" echo "Supabase not configured. Local log at ~/.gstack/security/attempts.jsonl" echo "still captures every attempt — tail it with:" echo " cat ~/.gstack/security/attempts.jsonl | tail -20" exit 0 fi # Fetch with the HTTP status captured (#1947). A backend failure must read # as "unknown", never as a healthy "0 attacks" — fake zeros on a security # surface are indistinguishable from good news. TMPBODY="$(mktemp)" trap 'rm -f "$TMPBODY"' EXIT HTTP_CODE="$(curl -s --max-time 15 -w '%{http_code}' -o "$TMPBODY" \ "${SUPABASE_URL}/functions/v1/community-pulse" \ -H "apikey: ${ANON_KEY}" \ 2>/dev/null || true)" # curl prints its own 000 before a non-zero exit — a `|| echo` here would # double it to "000000" in user-facing output. Normalize to the last 3 chars. HTTP_CODE="$(printf '%s' "$HTTP_CODE" | tr -d '[:space:]' | tail -c 3)" [ -n "$HTTP_CODE" ] || HTTP_CODE="000" DATA="$(cat "$TMPBODY" 2>/dev/null || echo "")" # Classify the response: # ok — 200 from the new backend (carries "status":"ok"); figures authoritative # legacy — 200 with a security section but no marker (pre-#1947 backend); # figures shown but flagged unverified (old backend masked errors as zeros) # unknown — non-200 / network failure / error body / missing section / no jq STATE="ok" REASON="" if [ "$HTTP_CODE" != "200" ] || [ -z "$DATA" ]; then STATE="unknown"; REASON="backend_error" elif ! command -v jq >/dev/null 2>&1; then # No lossy-grep fallback: the old regex broke on nested arrays and # under-reported attacks as zero. Without jq the honest answer is unknown. STATE="unknown"; REASON="jq_missing" elif ! echo "$DATA" | jq -e '.security' >/dev/null 2>&1; then STATE="unknown"; REASON="backend_error" elif [ "$(echo "$DATA" | jq -r '.status // empty' 2>/dev/null)" != "ok" ]; then STATE="legacy" fi if [ "$JSON_MODE" = "1" ]; then case "$STATE" in unknown) echo "{\"security\":null,\"status\":\"unknown\",\"reason\":\"${REASON}\"}" ;; legacy) echo "$DATA" | jq -c '{security: .security, status: "legacy_unverified"}' ;; ok) echo "$DATA" | jq -c '{security: .security, status: "ok", stale: (.stale // false)}' ;; esac exit 0 fi # Human-readable dashboard echo "gstack security dashboard" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo "" if [ "$STATE" = "unknown" ]; then if [ "$REASON" = "jq_missing" ]; then echo "Attacks detected last 7 days: unknown — install jq for exact figures" else echo "Attacks detected last 7 days: unknown — backend error (HTTP ${HTTP_CODE})" fi echo "" echo "Your local log: ~/.gstack/security/attempts.jsonl" echo "Your telemetry mode: $(${GSTACK_DIR}/bin/gstack-config get telemetry 2>/dev/null || echo unknown)" exit 0 fi # jq is guaranteed here (jq-missing classified as unknown above). The old # grep chain matched the digit 7 inside "attacks_last_7_days" itself and # misreported every count as 7. TOTAL="$(echo "$DATA" | jq -r '.security.attacks_last_7_days // 0' 2>/dev/null || echo "0")" echo "Attacks detected last 7 days: ${TOTAL}" if [ "$STATE" = "legacy" ]; then echo " (unverified — legacy backend response; deploy the latest community-pulse for verified figures)" elif [ "$(echo "$DATA" | jq -r '.stale // false' 2>/dev/null)" = "true" ]; then # The backend serves its last good snapshot when recompute fails — figures # are real but frozen. Don't present them as current. echo " (stale snapshot — backend recompute failing; figures may be out of date)" elif [ "$TOTAL" = "0" ]; then echo " (No attack attempts reported by the community yet. Good news.)" fi echo "" # Array sections — jq is guaranteed past the state gate; the old sed/grep # parsing truncated at the first ']' and dropped entries on any nesting # (the same bug class as the "every count is 7" TOTAL grep). DOMAINS="$(echo "$DATA" | jq -r '.security.top_attack_domains[]? | "\(.domain)\t\(.count)"' 2>/dev/null)" if [ -n "$DOMAINS" ]; then echo "Top attacked domains" echo "────────────────────" printf '%s\n' "$DOMAINS" | head -10 | while IFS="$(printf '\t')" read -r DOMAIN COUNT; do [ -n "$DOMAIN" ] && [ -n "$COUNT" ] && printf " %-40s %s attempts\n" "$DOMAIN" "$COUNT" done echo "" fi # Which layer catches attacks LAYERS="$(echo "$DATA" | jq -r '.security.top_attack_layers[]? | "\(.layer)\t\(.count)"' 2>/dev/null)" if [ -n "$LAYERS" ]; then echo "Top detection layers" echo "────────────────────" printf '%s\n' "$LAYERS" | while IFS="$(printf '\t')" read -r LAYER COUNT; do [ -n "$LAYER" ] && [ -n "$COUNT" ] && printf " %-28s %s\n" "$LAYER" "$COUNT" done echo "" fi # Verdict distribution VERDICTS="$(echo "$DATA" | jq -r '.security.verdict_distribution[]? | "\(.verdict)\t\(.count)"' 2>/dev/null)" if [ -n "$VERDICTS" ]; then echo "Verdict distribution" echo "────────────────────" printf '%s\n' "$VERDICTS" | while IFS="$(printf '\t')" read -r VERDICT COUNT; do [ -n "$VERDICT" ] && [ -n "$COUNT" ] && printf " %-14s %s\n" "$VERDICT" "$COUNT" done echo "" fi echo "Your local log: ~/.gstack/security/attempts.jsonl" echo "Your telemetry mode: $(${GSTACK_DIR}/bin/gstack-config get telemetry 2>/dev/null || echo unknown)"