mirror of
https://github.com/garrytan/gstack.git
synced 2026-05-02 03:35:09 +02:00
feat(supabase): community-pulse aggregates attack telemetry
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) <noreply@anthropic.com>
This commit is contained in:
@@ -102,12 +102,56 @@ Deno.serve(async () => {
|
|||||||
.slice(0, 5)
|
.slice(0, 5)
|
||||||
.map(([version, count]) => ({ version, count }));
|
.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<string, number> = {};
|
||||||
|
const layerCounts: Record<string, number> = {};
|
||||||
|
const verdictCounts: Record<string, number> = {};
|
||||||
|
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 = {
|
const result = {
|
||||||
weekly_active: current,
|
weekly_active: current,
|
||||||
change_pct: changePct,
|
change_pct: changePct,
|
||||||
top_skills: topSkills,
|
top_skills: topSkills,
|
||||||
crashes: crashes ?? [],
|
crashes: crashes ?? [],
|
||||||
versions: topVersions,
|
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
|
// Upsert cache
|
||||||
@@ -128,7 +172,19 @@ Deno.serve(async () => {
|
|||||||
});
|
});
|
||||||
} catch {
|
} catch {
|
||||||
return new Response(
|
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,
|
status: 200,
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
|
|||||||
Reference in New Issue
Block a user