feat(prober): publish attacker.fingerprinted on the bus (DEBT-031)

Each successful JARM / HASSH / TCPfp probe fans out an
attacker.fingerprinted event; the probe family goes in event.type so a
single subscription covers all three.  Payload carries the attacker IP,
port, and probe-specific hash — enough for the MazeNET live map to
render fingerprint info on observed attackers.

Lifts the thread-safe publisher helper out of the sniffer worker into
decnet/bus/publish.py so the prober (and every future worker with a
to_thread hot path) can reuse it without copy-pasting the
run_coroutine_threadsafe dance.  Sniffer rewires onto the shared helper
in passing.

Adds ATTACKER_FINGERPRINTED as a new leaf — distinct from
ATTACKER_OBSERVED (correlator's first-sight signal) because an active
probe result is additional evidence about an already-observed attacker.

Note: the plan's decky.{id}.state realism-probe publish path is
deferred — the current prober fingerprints attackers, not decky
realism.  Will revisit when realism probes exist.
This commit is contained in:
2026-04-21 16:47:55 -04:00
parent 7f497ac552
commit 34d9e37ab0
8 changed files with 322 additions and 40 deletions

View File

@@ -22,7 +22,7 @@ import pytest_asyncio
from decnet.bus import topics as _topics
from decnet.bus.fake import FakeBus
from decnet.sniffer.fingerprint import SnifferEngine
from decnet.sniffer.worker import _make_thread_safe_publisher
from decnet.sniffer.worker import _make_decky_traffic_publisher
@pytest_asyncio.fixture
@@ -145,7 +145,7 @@ async def test_sniffer_worker_degrades_cleanly_when_bus_disabled(
@pytest.mark.asyncio
async def test_thread_safe_publisher_routes_to_decky_traffic_topic(bus: FakeBus) -> None:
loop = asyncio.get_running_loop()
publish = _make_thread_safe_publisher(bus, loop)
publish = _make_decky_traffic_publisher(bus, loop)
sub = bus.subscribe(f"{_topics.DECKY}.*.{_topics.DECKY_TRAFFIC}")
async with sub: