refactor(prober): generalise ActiveProbe registry to absorb Ipv6LeakProbe

ActiveProbe.run/syslog_fields/publish_payload now accept port=None so
non-port-iterating probes can live in the registry. Ipv6LeakProbe replaces
the hand-rolled _ipv6_leak_phase special case in worker.py; it runs last
via priority=999. _probe_cycle no longer has an ad-hoc phase call.

Fixes three stale test files (test_prober_bus, test_prober_rotation,
test_prober_worker) that were broken since the 916b21b6 registry refactor.
This commit is contained in:
2026-05-21 14:27:48 -04:00
parent b80e621904
commit bd4700770b
12 changed files with 409 additions and 420 deletions

View File

@@ -1,59 +1,72 @@
"""Integration test: prober phase functions invoke the rotation recorder.
"""Integration test: _run_probe threads the rotation recorder through to probes.
The prober worker constructs the recorder closure at startup; here we
verify that ``_probe_cycle`` threads a recorder through to JARM / HASSH
/ TCPFP phases and that the recorder gets the (ip, port, probe_type,
hash) tuple it expects. The library itself is unit-tested separately.
verify that _run_probe calls record_rotation with (ip, port, probe_type,
hash) for JARM / HASSH / TCPFP on a successful probe, and that omitting
record_rotation is a safe no-op.
"""
from __future__ import annotations
from pathlib import Path
from unittest.mock import MagicMock, patch
from decnet.prober.worker import _probe_cycle
from decnet.prober.worker import _run_probe
@patch("decnet.prober.worker.fetch_leaf_cert", return_value=None)
@patch("decnet.prober.worker.tcp_fingerprint", return_value=None)
@patch("decnet.prober.worker.hassh_server", return_value=None)
@patch("decnet.prober.worker.jarm_hash")
def test_jarm_phase_calls_recorder(
mock_jarm: MagicMock,
_mock_hassh: MagicMock,
_mock_tcpfp: MagicMock,
_mock_cert: MagicMock,
tmp_path: Path,
):
mock_jarm.return_value = "c0c" * 10 + "a" * 32
log_path = tmp_path / "decnet.log"
json_path = tmp_path / "decnet.json"
rec_calls: list[tuple] = []
recorder = lambda ip, port, ptype, h: rec_calls.append((ip, port, ptype, h)) # noqa: E731
# ─── Helpers ─────────────────────────────────────────────────────────────────
_probe_cycle(
{"10.0.0.5"}, {},
[443], [], [],
log_path, json_path,
timeout=1.0,
publish_fn=None,
record_rotation=recorder,
)
def _recorder():
calls: list[tuple] = []
return calls, lambda ip, port, ptype, h: calls.append((ip, port, ptype, h))
# ─── JARM ────────────────────────────────────────────────────────────────────
def test_jarm_phase_calls_recorder(tmp_path: Path) -> None:
from decnet.prober.probes.jarm import JarmProbe
rec_calls, recorder = _recorder()
probe = JarmProbe()
probe._ports = [443]
with (
patch("decnet.prober.probes.jarm.jarm_hash", return_value="c0c" * 10 + "a" * 32),
patch("decnet.prober.worker.fetch_leaf_cert", return_value=None),
):
_run_probe(
probe, "10.0.0.5", {},
tmp_path / "decnet.log", tmp_path / "decnet.json",
timeout=1.0, publish_fn=None, record_rotation=recorder,
)
assert rec_calls == [("10.0.0.5", 443, "jarm", "c0c" * 10 + "a" * 32)]
@patch("decnet.prober.worker.fetch_leaf_cert", return_value=None)
@patch("decnet.prober.worker.tcp_fingerprint", return_value=None)
@patch("decnet.prober.worker.hassh_server")
@patch("decnet.prober.worker.jarm_hash", return_value="")
def test_hassh_phase_calls_recorder(
_mock_jarm: MagicMock,
mock_hassh: MagicMock,
_mock_tcpfp: MagicMock,
_mock_cert: MagicMock,
tmp_path: Path,
):
mock_hassh.return_value = {
def test_jarm_phase_no_recorder_call_on_empty_hash(tmp_path: Path) -> None:
from decnet.prober.probes.jarm import JarmProbe
from decnet.prober.jarm import JARM_EMPTY_HASH
rec_calls, recorder = _recorder()
probe = JarmProbe()
probe._ports = [443]
with patch("decnet.prober.probes.jarm.jarm_hash", return_value=JARM_EMPTY_HASH):
_run_probe(
probe, "10.0.0.5", {},
tmp_path / "decnet.log", tmp_path / "decnet.json",
timeout=1.0, publish_fn=None, record_rotation=recorder,
)
assert rec_calls == []
# ─── HASSH ───────────────────────────────────────────────────────────────────
def test_hassh_phase_calls_recorder(tmp_path: Path) -> None:
from decnet.prober.probes.hassh import HasshProbe
rec_calls, recorder = _recorder()
probe = HasshProbe()
probe._ports = [22]
stub = {
"hassh_server": "deadbeef",
"banner": "SSH-2.0-OpenSSH_9.2",
"kex_algorithms": "x",
@@ -61,82 +74,56 @@ def test_hassh_phase_calls_recorder(
"mac_s2c": "x",
"compression_s2c": "x",
}
log_path = tmp_path / "decnet.log"
json_path = tmp_path / "decnet.json"
rec_calls: list[tuple] = []
recorder = lambda ip, port, ptype, h: rec_calls.append((ip, port, ptype, h)) # noqa: E731
_probe_cycle(
{"10.0.0.5"}, {},
[], [22], [],
log_path, json_path,
timeout=1.0,
publish_fn=None,
record_rotation=recorder,
)
with patch("decnet.prober.probes.hassh.hassh_server", return_value=stub):
_run_probe(
probe, "10.0.0.5", {},
tmp_path / "decnet.log", tmp_path / "decnet.json",
timeout=1.0, publish_fn=None, record_rotation=recorder,
)
assert rec_calls == [("10.0.0.5", 22, "hassh", "deadbeef")]
@patch("decnet.prober.worker.fetch_leaf_cert", return_value=None)
@patch("decnet.prober.worker.tcp_fingerprint")
@patch("decnet.prober.worker.hassh_server", return_value=None)
@patch("decnet.prober.worker.jarm_hash", return_value="")
def test_tcpfp_phase_calls_recorder(
_mock_jarm, _mock_hassh, mock_tcpfp, _mock_cert, tmp_path: Path,
):
mock_tcpfp.return_value = {
# ─── TCPFP ───────────────────────────────────────────────────────────────────
def test_tcpfp_phase_calls_recorder(tmp_path: Path) -> None:
from decnet.prober.probes.tcpfp import TcpfpProbe
rec_calls, recorder = _recorder()
probe = TcpfpProbe()
probe._ports = [22]
stub = {
"tcpfp_hash": "tcpfp-hash-1",
"tcpfp_raw": "raw",
"ttl": 64,
"window_size": 65535,
"df_bit": True,
"mss": 1460,
"window_scale": 7,
"sack_ok": True,
"timestamp": True,
"options_order": "MSS,SACK,TS,NOP,WS",
"tos": 0,
"dscp": 0,
"ecn": 0,
"server_isn": 0,
"ttl": 64, "window_size": 65535, "df_bit": True,
"mss": 1460, "window_scale": 7, "sack_ok": True,
"timestamp": True, "options_order": "MSS,SACK,TS,NOP,WS",
"tos": 0, "dscp": 0, "ecn": 0, "server_isn": 0,
}
log_path = tmp_path / "decnet.log"
json_path = tmp_path / "decnet.json"
rec_calls: list[tuple] = []
recorder = lambda ip, port, ptype, h: rec_calls.append((ip, port, ptype, h)) # noqa: E731
_probe_cycle(
{"10.0.0.5"}, {},
[], [], [22],
log_path, json_path,
timeout=1.0,
publish_fn=None,
record_rotation=recorder,
)
with patch("decnet.prober.probes.tcpfp.tcp_fingerprint", return_value=stub):
_run_probe(
probe, "10.0.0.5", {},
tmp_path / "decnet.log", tmp_path / "decnet.json",
timeout=1.0, publish_fn=None, record_rotation=recorder,
)
assert rec_calls == [("10.0.0.5", 22, "tcpfp", "tcpfp-hash-1")]
@patch("decnet.prober.worker.fetch_leaf_cert", return_value=None)
@patch("decnet.prober.worker.tcp_fingerprint", return_value=None)
@patch("decnet.prober.worker.hassh_server", return_value=None)
@patch("decnet.prober.worker.jarm_hash")
def test_recorder_optional_no_crash_when_none(
mock_jarm: MagicMock,
_mock_hassh: MagicMock,
_mock_tcpfp: MagicMock,
_mock_cert: MagicMock,
tmp_path: Path,
):
"""record_rotation=None must keep the prober's pre-DEBT-032 behavior."""
mock_jarm.return_value = "c0c" * 10 + "a" * 32
_probe_cycle(
{"10.0.0.5"}, {},
[443], [], [],
tmp_path / "decnet.log", tmp_path / "decnet.json",
timeout=1.0,
publish_fn=None,
record_rotation=None,
)
# No error, probe completes.
# ─── Safety ──────────────────────────────────────────────────────────────────
def test_recorder_optional_no_crash_when_none(tmp_path: Path) -> None:
"""record_rotation=None must keep pre-DEBT-032 behavior — no crash."""
from decnet.prober.probes.jarm import JarmProbe
probe = JarmProbe()
probe._ports = [443]
with (
patch("decnet.prober.probes.jarm.jarm_hash", return_value="c0c" * 10 + "a" * 32),
patch("decnet.prober.worker.fetch_leaf_cert", return_value=None),
):
_run_probe(
probe, "10.0.0.5", {},
tmp_path / "decnet.log", tmp_path / "decnet.json",
timeout=1.0, publish_fn=None, record_rotation=None,
)