From 2d107978498ee64783ddb1b41103c9d18366208d Mon Sep 17 00:00:00 2001 From: Garry Tan Date: Mon, 20 Apr 2026 04:58:08 +0800 Subject: [PATCH] feat(supabase): community-pulse aggregates attack telemetry MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a `security` section to the community-pulse response: security: { attacks_last_7_days: number, top_attack_domains: [{ domain, count }], top_attack_layers: [{ layer, count }], verdict_distribution: [{ verdict, count }], } Queries telemetry_events WHERE event_type = 'attack_attempt' over the last 7 days, groups by domain/layer/verdict client-side in the edge function (matches the existing top_skills aggregation pattern). Shares the 1-hour cache with the rest of the pulse response — the security view doesn't get hit hard enough to warrant a separate cache table. Attack data updates once an hour for read-path consumers. Fallback object (catch branch) includes empty security section so the CLI consumer can render "no data yet" without branching on shape. Co-Authored-By: Claude Opus 4.7 (1M context) --- supabase/functions/community-pulse/index.ts | 58 ++++++++++++++++++++- 1 file changed, 57 insertions(+), 1 deletion(-) diff --git a/supabase/functions/community-pulse/index.ts b/supabase/functions/community-pulse/index.ts index acf2fdb7..682d3d3f 100644 --- a/supabase/functions/community-pulse/index.ts +++ b/supabase/functions/community-pulse/index.ts @@ -102,12 +102,56 @@ Deno.serve(async () => { .slice(0, 5) .map(([version, count]) => ({ version, count })); + // Security events — aggregate attack_attempt events from the last 7 days. + // Fields emitted by gstack-telemetry-log --event-type attack_attempt: + // security_url_domain, security_payload_hash, security_confidence, + // security_layer, security_verdict. + const { data: attackRows } = await supabase + .from("telemetry_events") + .select("security_url_domain, security_layer, security_verdict") + .eq("event_type", "attack_attempt") + .gte("event_timestamp", weekAgo) + .limit(5000); + + const attacksTotal = attackRows?.length ?? 0; + const domainCounts: Record = {}; + const layerCounts: Record = {}; + const verdictCounts: Record = {}; + for (const row of attackRows ?? []) { + if (row.security_url_domain) { + domainCounts[row.security_url_domain] = (domainCounts[row.security_url_domain] ?? 0) + 1; + } + if (row.security_layer) { + layerCounts[row.security_layer] = (layerCounts[row.security_layer] ?? 0) + 1; + } + if (row.security_verdict) { + verdictCounts[row.security_verdict] = (verdictCounts[row.security_verdict] ?? 0) + 1; + } + } + const topAttackDomains = Object.entries(domainCounts) + .sort(([, a], [, b]) => b - a) + .slice(0, 10) + .map(([domain, count]) => ({ domain, count })); + const topAttackLayers = Object.entries(layerCounts) + .sort(([, a], [, b]) => b - a) + .map(([layer, count]) => ({ layer, count })); + const attackVerdictDistribution = Object.entries(verdictCounts) + .sort(([, a], [, b]) => b - a) + .map(([verdict, count]) => ({ verdict, count })); + const result = { weekly_active: current, change_pct: changePct, top_skills: topSkills, crashes: crashes ?? [], versions: topVersions, + // Security aggregate for the /security-dashboard view + security: { + attacks_last_7_days: attacksTotal, + top_attack_domains: topAttackDomains, + top_attack_layers: topAttackLayers, + verdict_distribution: attackVerdictDistribution, + }, }; // Upsert cache @@ -128,7 +172,19 @@ Deno.serve(async () => { }); } catch { return new Response( - JSON.stringify({ weekly_active: 0, change_pct: 0, top_skills: [], crashes: [], versions: [] }), + JSON.stringify({ + weekly_active: 0, + change_pct: 0, + top_skills: [], + crashes: [], + versions: [], + security: { + attacks_last_7_days: 0, + top_attack_domains: [], + top_attack_layers: [], + verdict_distribution: [], + }, + }), { status: 200, headers: { "Content-Type": "application/json" },