feat(sniffer): ISN sequence classifier (reuses seq_class helper)

Mirrors the IP-ID classifier for TCP ISN values: per-source-IP rolling
deque (maxlen=8) populated from each inbound SYN's tcp.seq, classified
on every emission. A 'random' verdict is the modern norm; 'incremental',
'zero', or 'constant' indicates legacy stacks or hand-rolled raw-socket
tooling — a strong fingerprint signal.

Active prober now also captures server_isn (single sample, not classified
in-flight; downstream consumers correlating multi-probe results can apply
seq_class.classify_sequence themselves).

Profiler rollup carries the latest non-'unknown' label into
attacker.tcp_fingerprint. Dedup key already covers isn_class from
the previous commit, so transitions emit cleanly.

UI surfaces ISN class as a colour-coded tag with a ⚠ glyph for
non-random verdicts, since they're the genuinely interesting case.
This commit is contained in:
2026-04-26 20:30:24 -04:00
parent 0e40cc8ae1
commit c595d039bd
7 changed files with 67 additions and 0 deletions

View File

@@ -144,6 +144,7 @@ def sniffer_rollup(events: list[LogEvent]) -> dict[str, Any]:
hops: list[int] = []
tcp_fp: dict[str, Any] | None = None
ipid_latest: str | None = None
isn_latest: str | None = None
# Tracks which event set tcp_fp last — picks the provider "context"
# (syn vs synack) when we feed the p0f-v2 matcher below.
tcp_fp_context: str = "syn"
@@ -193,6 +194,10 @@ def sniffer_rollup(events: list[LogEvent]) -> dict[str, Any]:
if ipid_class and ipid_class != "unknown":
ipid_latest = ipid_class
tcp_fp["ipid_class"] = ipid_latest
isn_class = e.fields.get("isn_class")
if isn_class and isn_class != "unknown":
isn_latest = isn_class
tcp_fp["isn_class"] = isn_latest
tcp_fp_context = "syn"
elif e.event_type == _SNIFFER_FLOW_EVENT: