mirror of
https://github.com/garrytan/gstack.git
synced 2026-10-31 22:14:25 +01:00
Bare mktemp, mktemp -t and literal /tmp templates in the learnings,
question and preference logs (plus jsonl-merge, distill, the two
dashboards, /ship's PR body and /document-release's run dir) now use
"${TMPDIR:-/tmp}/<name>.XXXXXX", the form the INV-3 lint accepts. When
the temp file cannot be created, the learnings, question and preference
logs now say the entry was not recorded and how to fix TMPDIR instead of
exiting with only mktemp's own error. Three tests that pinned the bare
PR_BODY_FILE=$(mktemp) and one that pinned the /tmp run dir migrate.
GNU and BSD mktemp already honor TMPDIR without a template, so the
macOS-sandbox drop in #2952 is unverified here; the template makes the
temp location explicit on every mktemp.
Co-authored-by: Davon Johnson <d.johnson@dayofshow.net>
167 lines
7.0 KiB
Bash
Executable File
167 lines
7.0 KiB
Bash
Executable File
#!/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)}"
|
|
|
|
# Egress receipt helpers (_receipted_curl): fail-open for read-only stats.
|
|
. "$GSTACK_DIR/bin/gstack-egress-lib.sh"
|
|
|
|
# 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 "${TMPDIR:-/tmp}/gstack-security.XXXXXX")"
|
|
trap 'rm -f "$TMPBODY"' EXIT
|
|
SUPA_HOST="${SUPABASE_URL#*://}"; SUPA_HOST="${SUPA_HOST%%/*}"
|
|
HTTP_CODE="$(_receipted_curl open security-dashboard "$SUPA_HOST" community-pulse-fetch "user-invoked dashboard" --no-payload \
|
|
curl -s --max-time 15 -w '%{http_code}' -o "$TMPBODY" \
|
|
"${SUPABASE_URL}/functions/v1/community-pulse" \
|
|
-H "apikey: ${ANON_KEY}" || 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)"
|