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" },