From 02e73a19d561eaa5fac19ebeb3c410c763636177 Mon Sep 17 00:00:00 2001 From: anti Date: Wed, 15 Apr 2026 16:44:45 -0400 Subject: [PATCH] fix: promote TCP-fingerprinted nmap to tool_guesses (detects -sC sans HTTP) --- decnet/profiler/behavioral.py | 7 +++++++ tests/test_profiler_behavioral.py | 12 ++++++++++++ 2 files changed, 19 insertions(+) diff --git a/decnet/profiler/behavioral.py b/decnet/profiler/behavioral.py index f8d3283..bd19acc 100644 --- a/decnet/profiler/behavioral.py +++ b/decnet/profiler/behavioral.py @@ -504,6 +504,13 @@ def build_behavior_record(events: list[LogEvent]) -> dict[str, Any]: header_tools = detect_tools_from_headers(events) all_tools: list[str] = list(dict.fromkeys(beacon_tools + header_tools)) # dedup, preserve order + # Promote TCP-level scanner identification to tool_guesses. + # p0f fingerprints nmap from the TCP handshake alone — this fires even + # when no HTTP service is present, making it far more reliable than the + # header-based path for raw port scans. + if rollup["os_guess"] == "nmap" and "nmap" not in all_tools: + all_tools.insert(0, "nmap") + # Beacon-specific projection: only surface interval/jitter when we've # classified the flow as beaconing (otherwise these numbers are noise). beacon_interval_s: float | None = None diff --git a/tests/test_profiler_behavioral.py b/tests/test_profiler_behavioral.py index f444329..eb18a1b 100644 --- a/tests/test_profiler_behavioral.py +++ b/tests/test_profiler_behavioral.py @@ -475,3 +475,15 @@ class TestBuildBehaviorRecord: events = [_mk(i * 300.0) for i in range(5)] # 5-min intervals, no signature match r = build_behavior_record(events) assert json.loads(r["tool_guesses"]) == [] + + def test_nmap_promoted_from_tcp_fingerprint(self): + # p0f identifies nmap from TCP handshake → must appear in tool_guesses + # even when no HTTP request events are present. + events = [ + _mk(0, event_type="tcp_syn_fingerprint", service="ssh", + fields={"os_guess": "nmap", "window": "31337", "ttl": "58"}), + _mk(1, event_type="tcp_syn_fingerprint", service="smb", + fields={"os_guess": "nmap", "window": "31337", "ttl": "58"}), + ] + r = build_behavior_record(events) + assert "nmap" in json.loads(r["tool_guesses"])