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.
28 lines
843 B
Python
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
|