Files
DECNET/decnet/web/router/attackers/_guards.py
anti bb77d13f9a feat(api/attackers): per-attacker SSE events stream
GET /api/v1/attackers/{uuid}/events streams behavioural events for
one attacker. Mirrors decnet/web/router/topology/api_events.py
end-to-end: ?token= auth, require_stream_viewer gate,
sse_connection_slot per-user cap, snapshot-on-connect, three bus
subscriptions (attacker.observation.>, attacker.fingerprint_rotated,
attacker.scored) merged through asyncio.Queue, 15s keepalive,
request.is_disconnected() exit, finally task cancellation.

Per-attacker filter keys on payload['attacker_uuid'] which the
profiler worker stamps onto every published payload (Phase 5 P5.0
amendment) — O(1) drop without a repo round-trip per event.

_sse_name_for derives SSE event names:
  attacker.observation.<primitive> → observation.<primitive>
  attacker.fingerprint_rotated     → fingerprint.rotated
  attacker.scored                  → attacker.scored

10 tests cover snapshot, live forward, per-attacker filter (drops
other attackers' events), fingerprint.rotated forward, 404, 401, and
the sse-name derivation across all four cases. Topology events
regression green.
2026-05-08 20:23:29 -04:00

28 lines
843 B
Python

"""Shared helpers for the per-attacker routes.
Currently houses the 404 guard used by the SSE events stream
(:mod:`api_events`). Mirrors the topology router's
``_guards.get_topology_or_404`` shape so a future grep for "guard"
finds both.
"""
from __future__ import annotations
from typing import Any
from fastapi import HTTPException
from decnet.web.dependencies import repo
async def get_attacker_or_404(attacker_uuid: str) -> dict[str, Any]:
"""Fetch an Attacker row by UUID or raise 404.
The 404 fires *after* auth (the route's role gate runs first), so
an existence probe can't leak a UUID's presence to an
unauthenticated caller.
"""
attacker = await repo.get_attacker_by_uuid(attacker_uuid)
if not attacker:
raise HTTPException(status_code=404, detail="Attacker not found")
return attacker