From 756875a7343c1785173c34a603620b9512462816 Mon Sep 17 00:00:00 2001 From: Garry Tan Date: Mon, 20 Apr 2026 04:58:09 +0800 Subject: [PATCH] feat(dashboard): add gstack-security-dashboard CLI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New bash CLI at bin/gstack-security-dashboard that consumes the security section of the community-pulse edge function response and renders: * Attacks detected last 7 days (total) * Top attacked domains (up to 10) * Top detection layers (which security stack layer catches most) * Verdict distribution (block / warn / log_only split) * Pointer to local log + user's telemetry mode Two modes: * Default — human-readable dashboard, same visual style as bin/gstack-community-dashboard * --json — machine-readable shape for scripts and CI Graceful degradation when Supabase isn't configured: prints a helpful message pointing to the local ~/.gstack/security/attempts.jsonl log. Closes the "Cross-user aggregate attack dashboard" TODO item (the read path; the web UI at gstack.gg/dashboard/security is still a separate webapp project). Co-Authored-By: Claude Opus 4.7 (1M context) --- bin/gstack-security-dashboard | 114 ++++++++++++++++++++++++++++++++++ 1 file changed, 114 insertions(+) create mode 100755 bin/gstack-security-dashboard diff --git a/bin/gstack-security-dashboard b/bin/gstack-security-dashboard new file mode 100755 index 00000000..910aa21b --- /dev/null +++ b/bin/gstack-security-dashboard @@ -0,0 +1,114 @@ +#!/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 + +DATA="$(curl -sf --max-time 15 \ + "${SUPABASE_URL}/functions/v1/community-pulse" \ + -H "apikey: ${ANON_KEY}" \ + 2>/dev/null || echo "{}")" + +# Extract the security section +SEC_SECTION="$(echo "$DATA" | grep -o '"security":{[^}]*}' 2>/dev/null || echo "")" + +if [ "$JSON_MODE" = "1" ]; then + # Machine-readable — echo the whole security section (or empty object) + if [ -n "$SEC_SECTION" ]; then + echo "{${SEC_SECTION}}" + else + echo '{"security":{"attacks_last_7_days":0,"top_attack_domains":[],"top_attack_layers":[],"verdict_distribution":[]}}' + fi + exit 0 +fi + +# Human-readable dashboard +echo "gstack security dashboard" +echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" +echo "" + +TOTAL="$(echo "$DATA" | grep -o '"attacks_last_7_days":[0-9]*' | grep -o '[0-9]*' | head -1 || echo "0")" +echo "Attacks detected last 7 days: ${TOTAL}" +if [ "$TOTAL" = "0" ]; then + echo " (No attack attempts reported by the community yet. Good news.)" +fi +echo "" + +# Top attacked domains — parse objects inside top_attack_domains array +DOMAINS="$(echo "$DATA" | sed -n 's/.*"top_attack_domains":\(\[[^]]*\]\).*/\1/p' | head -1)" +if [ -n "$DOMAINS" ] && [ "$DOMAINS" != "[]" ]; then + echo "Top attacked domains" + echo "────────────────────" + echo "$DOMAINS" | grep -o '{[^}]*}' | head -10 | while read -r OBJ; do + DOMAIN="$(echo "$OBJ" | grep -o '"domain":"[^"]*"' | awk -F'"' '{print $4}')" + COUNT="$(echo "$OBJ" | grep -o '"count":[0-9]*' | grep -o '[0-9]*')" + [ -n "$DOMAIN" ] && [ -n "$COUNT" ] && printf " %-40s %s attempts\n" "$DOMAIN" "$COUNT" + done + echo "" +fi + +# Which layer catches attacks +LAYERS="$(echo "$DATA" | sed -n 's/.*"top_attack_layers":\(\[[^]]*\]\).*/\1/p' | head -1)" +if [ -n "$LAYERS" ] && [ "$LAYERS" != "[]" ]; then + echo "Top detection layers" + echo "────────────────────" + echo "$LAYERS" | grep -o '{[^}]*}' | while read -r OBJ; do + LAYER="$(echo "$OBJ" | grep -o '"layer":"[^"]*"' | awk -F'"' '{print $4}')" + COUNT="$(echo "$OBJ" | grep -o '"count":[0-9]*' | grep -o '[0-9]*')" + [ -n "$LAYER" ] && [ -n "$COUNT" ] && printf " %-28s %s\n" "$LAYER" "$COUNT" + done + echo "" +fi + +# Verdict distribution +VERDICTS="$(echo "$DATA" | sed -n 's/.*"verdict_distribution":\(\[[^]]*\]\).*/\1/p' | head -1)" +if [ -n "$VERDICTS" ] && [ "$VERDICTS" != "[]" ]; then + echo "Verdict distribution" + echo "────────────────────" + echo "$VERDICTS" | grep -o '{[^}]*}' | while read -r OBJ; do + VERDICT="$(echo "$OBJ" | grep -o '"verdict":"[^"]*"' | awk -F'"' '{print $4}')" + COUNT="$(echo "$OBJ" | grep -o '"count":[0-9]*' | grep -o '[0-9]*')" + [ -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)"