Skip to content

Commit 125ac07

Browse files
bandrelclaude
andcommitted
docs: correct honeypot detection docs for operators
- CLAUDE.md: HONEYPOT_SIGNATURES ships empty in this release; no active generic signatures, the Heralding VNC tell is the only named-product match the code can produce, and the generic mechanism is retained solely as an extensibility point for future entries verified against real -sV output. - CLAUDE.md + config.json.sample + the __honeypot_active_confirm_note__ constant in spoonmap.py: the active probe runs on hosts flagged by the TTL-spread or port-profile signals, not on "any flagged host" — a host flagged only by the older tarpit open-port-ratio heuristic feeds the finding but is never probed. - CLAUDE.md: document three limitations plainly, none of them fixed in code. (1) On a Full 1-65535 scan essentially every ephemeral port is already scanned, so _select_confirm_probe_ports() finds few or no candidates and the probe silently does nothing. (2) All honeypot detection lives in mass_scan(); the direct-nmap port-discovery path used below nmap_threshold performs none of it. (3) Every signal only becomes a FINDING when script_scan is true, since generate_findings() returns immediately without nse_results/ — the Stage 1 stdout warnings and discovery/*.txt files are written either way, because those come from mass_scan(). - CLAUDE.md: record the unconditional truncation of the two new discovery files, the pre-loop `enabled` check, and the new scope filter. - README.md: add the missing `honeypot_active_confirm` config-key row, and add "Likely Honeypot / Decoy Host" to the findings-severity catalog at all three tiers it can now emit, following the table's existing one-row-per- severity convention for multi-severity findings (cf. the SNMP and LLM-API entries). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
1 parent e0d1d97 commit 125ac07

4 files changed

Lines changed: 71 additions & 13 deletions

File tree

‎CLAUDE.md‎

Lines changed: 55 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -452,9 +452,20 @@ behind one address, though NAT/load-balancing can produce the same pattern,
452452
which is why this signal alone is capped at LOW and can never alone reach
453453
HIGH); known port-profile matching against `HONEYPOT_PORT_PROFILES`
454454
(Thinkst Canary, Artillery — placeholders pending verification against
455-
current upstream defaults); named-product signature matching against
456-
`HONEYPOT_SIGNATURES` (Cowrie/Kippo, Dionaea default banners — same
457-
verification caveat) via the -sV service data already captured; a
455+
current upstream defaults — the finding's own reason text carries that
456+
caveat verbatim, since it lands in an engagement deliverable); generic
457+
named-product signature matching against `HONEYPOT_SIGNATURES` via the -sV
458+
service data already captured, **which ships EMPTY in this release** — the
459+
two development-era entries (Cowrie/Kippo, Dionaea) were both verified
460+
incorrect against nmap's real output in final review and removed rather than
461+
patched, so there are no active generic signatures and the mechanism
462+
(`_honeypot_signature_match()`, `_named_honeypot_matches()`) is retained
463+
purely as an extensibility point for future entries verified against real
464+
`-sV` output rather than a synthetic fixture (the reasons are recorded at
465+
the tuple itself; the short version is that the Cowrie needle's hyphen can
466+
never match nmap's space-rendered template, and "fixing" it would
467+
false-positive HIGH on every ordinary Debian OpenSSH host, while the Dionaea
468+
needle only ever appears in `servicefp`, a field the matcher never reads); a
458469
source-verified Heralding VNC honeypot tell (`_vnc_heralding_match()`,
459470
Task 3 Step 7 — vnc-info reporting RFB protocol 3.7 with no security-type
460471
list, since Heralding's VNC capability hardcodes that exact version and
@@ -467,16 +478,20 @@ data came back and failed to match, so a LaBrea-style silent host previously
467478
scored zero on that check alone. `_iter_open_tcp_ports()` is the shared,
468479
per-element-defensive XML walk both nmap-side counters and the signature
469480
matcher use, factored out of what was originally
470-
`_count_unmatched_service_ports()`'s own walk. Severity is HIGH for a named
481+
`_count_unmatched_service_ports()`'s own walk. With `HONEYPOT_SIGNATURES`
482+
empty, the Heralding VNC tell is the only named-product match this release
483+
can produce. Severity is HIGH for a named
471484
signature match or an active-probe confirmation, MEDIUM for two or more
472485
heuristic signals together, LOW for exactly one — a behavior change from the
473486
single flat MEDIUM this finding used to emit unconditionally. An optional
474487
active confirmation probe (`_active_confirm_probe()`) attempts a raw TCP
475488
connect against a handful of high, never-scanned ports
476489
(`_select_confirm_probe_ports()`, deterministic per-IP via a seeded
477490
`random.Random(ip)` rather than global randomness, so it is reproducible and
478-
testable) on any flagged host; a real host has no reason to answer on an
479-
ephemeral port outside the scan, so an answer is treated as confirmation.
491+
testable) on any host flagged by the TTL-spread or port-profile signals —
492+
**not** on a host flagged only by the older tarpit-ratio heuristic, which
493+
feeds the finding but never the probe; a real host has no reason to answer on
494+
an ephemeral port outside the scan, so an answer is treated as confirmation.
480495
This is the one signal that sends new traffic, so it is gated behind
481496
`honeypot_active_confirm` in config.json — default false, absent means
482497
false, no interactive prompt of its own — mirroring `check_for_updates`'s
@@ -491,4 +506,37 @@ that pattern.) `discovery/suspected_honeypots.txt` (Stage 1: TTL spread and
491506
port-profile hits, one line per `(ip, signal)` pair) and
492507
`discovery/confirmed_honeypots.txt` (Stage 2: active-probe confirmations)
493508
are new sibling files to the existing `discovery/suspected_tarpits.txt`,
494-
read back the same defensively-parsed way in `generate_findings()`.
509+
read back the same defensively-parsed way in `generate_findings()`. Unlike
510+
`suspected_tarpits.txt`, both of those are written **unconditionally** — an
511+
empty result truncates the file rather than leaving the previous run's
512+
content in place. That divergence is deliberate: severity is now computed by
513+
*counting* signals, so a stale line from an earlier, broader run silently
514+
inflates a later, narrower run's severity, and a stale
515+
`confirmed_honeypots.txt` line additionally makes the deliverable assert
516+
"host answered on a port never scanned open" for a probe that did not run
517+
this time at all (exactly what happens when the operator turns
518+
`honeypot_active_confirm` back off). The `enabled` check happens in
519+
`_confirm_flagged_honeypots()` itself, ahead of the per-host loop, so the
520+
common disabled case skips `_select_confirm_probe_ports()`'s candidate-set
521+
work entirely — but that path still truncates the file rather than returning
522+
outright, for the reason just given. Every host handed to the probe is first
523+
run through `_scope_filtered_flags()` against the current `target_file`,
524+
since the probe is the only honeypot signal that *sends packets* and the
525+
flag dict is built from cached `live_hosts/` and `masscan_results/` data
526+
that a narrowed `ranges.txt` deliberately never prunes; an empty/unparseable
527+
scope is permissive, matching `_report_out_of_scope_retained()`.
528+
529+
**Three limitations an operator enabling `honeypot_active_confirm` needs to
530+
know.** (1) On a **Full** scan (`dest_ports == ['1-65535']`) essentially every
531+
ephemeral port has already been scanned, so `_select_confirm_probe_ports()`
532+
finds few or zero unscanned candidates and the probe effectively does
533+
nothing — silently, with no message either way. (2) **All** honeypot
534+
detection, Stage 1 signals and the active probe alike, lives inside
535+
`mass_scan()`; the alternate direct-nmap port-discovery path (taken for
536+
small/medium scans below the configured `nmap_threshold` work-unit ceiling)
537+
performs none of it. (3) Every signal in this feature only ever becomes a
538+
*finding* when `script_scan: true`, because `generate_findings()` returns
539+
immediately when `nse_results/` does not exist and the default is off — the
540+
Stage 1 stdout warnings and the `discovery/*.txt` files are still written
541+
either way, since those come from `mass_scan()`, not `generate_findings()`.
542+
None of the three is fixed in code; they are documented as-is.

‎README.md‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -308,6 +308,7 @@ git update-index --no-skip-worktree ranges.txt
308308
| `resume` | `"True"` / `"False"` | Skip completed port discovery on restart (default: False) |
309309
| `check_for_updates` | `"True"` / `"False"` | Contact api.github.com at startup to check for a newer release. Off unless set; see `--check-update` for a one-off check (default: False) |
310310
| `scanner_profile` | `false`, `"random"`, or an object | Substitutes strings that identify nmap/SpooNMAP in probe traffic (e.g. the RDP probe's `mstshash=nmap` cookie). Off unless set — see below |
311+
| `honeypot_active_confirm` | `"True"` / `"False"` | Actively confirm suspected decoy hosts with a few raw TCP connects to unscanned high ports. Off unless set; only probes in-scope hosts flagged by the TTL-spread or port-profile signals, and only on the masscan scan path (default: False) |
311312
| `__generated_by_prompts__` | String | Present only in a config the prompts wrote. While it is present, **[d]elete**/**[a]ppend** re-ask the options using this file's values as defaults; remove it to keep them fixed |
312313

313314
Keys beginning and ending with `__` are documentation and are ignored by the loader, so you can annotate the file freely — a re-prompted run preserves them.
@@ -554,6 +555,7 @@ After scanning, `generate_findings()` parses all nmap XML results and produces s
554555
| HIGH | IKE Aggressive Mode with Pre-Shared Key (U:500, confirmed by `ike-version`) |
555556
| HIGH | RealVNC Authentication Bypass (CVE-2006-2369) (5900/5901, confirmed by `realvnc-auth-bypass`) |
556557
| HIGH | Service Exposed Externally (databases, RDP, SMB, SNMP, WebLogic, VNC, WSUS 8530/8531, etc. — external scan only) |
558+
| HIGH | Likely Honeypot / Decoy Host — named product match (Heralding VNC) or an active confirmation-probe answer |
557559
| MEDIUM | SMBv1 protocol enabled |
558560
| MEDIUM | Weak SSH algorithms (deprecated ciphers/MACs/KEX) |
559561
| MEDIUM | Java RMI registry exposed |
@@ -563,6 +565,8 @@ After scanning, `generate_findings()` parses all nmap XML results and produces s
563565
| MEDIUM | OpenAI-Compatible LLM API Unauthenticated (1234/1337/3000/8000, custom NSE — internal scan) |
564566
| MEDIUM | Gradio LLM Web UI Accessible (7860, custom NSE — internal scan) |
565567
| MEDIUM | KoboldCpp LLM API Unauthenticated (5001, custom NSE — internal scan) |
568+
| MEDIUM | Likely Honeypot / Decoy Host — two or more heuristic signals on one host |
569+
| LOW | Likely Honeypot / Decoy Host — exactly one heuristic signal (tarpit open-port ratio, TTL spread, port profile, unmatched fingerprints, or silent open ports) |
566570
| LOW | Anonymous FTP login (default LOW — review the share; escalate if it exposes sensitive data or is writable) |
567571
| LOW | FTP exposed externally (plaintext protocol — credentials/data in cleartext; use FTPS/SFTP) |
568572
| LOW | Telnet exposed externally (plaintext protocol — credentials/data in cleartext; use SSH) |

‎config.json.sample‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
"resume" : false,
1919
"__check_for_updates_note__": "Optional. When true, SpooNMAP contacts api.github.com at startup to see whether a newer release exists. Default false, and absent means false: the tool makes no network connection other than the scan itself unless you turn this on. Use --check-update for a one-off check without enabling it here.",
2020
"check_for_updates": false,
21-
"__honeypot_active_confirm_note__": "Optional. When true, SpooNMAP attempts a small number of raw TCP connects against unscanned high ports on any host flagged as a suspected honeypot, to confirm it. Default false, and absent means false: the tool sends no traffic beyond the scan itself unless you turn this on. A confirmed host answered a port that was never part of this scan, which is a strong tell that it accepts every connection rather than running a real service.",
21+
"__honeypot_active_confirm_note__": "Optional. When true, SpooNMAP attempts a small number of raw TCP connects against unscanned high ports on any host flagged by the TTL-spread or port-profile signals, to confirm it. Hosts flagged only by the tarpit open-port-ratio heuristic are not probed. Default false, and absent means false: the tool sends no traffic beyond the scan itself unless you turn this on. A confirmed host answered a port that was never part of this scan, which is a strong tell that it accepts every connection rather than running a real service. Limitations: hosts outside target_file are never probed; on a Full (1-65535) scan there are almost no unscanned ports left to probe, so it does little or nothing; and honeypot detection as a whole only runs on the masscan path, not the direct-nmap path used for scans below nmap_threshold.",
2222
"honeypot_active_confirm": false,
2323
"__target_scan_choices__" : "External, Internal (case-insensitive; any other value stops the run rather than scanning with the Internal-only checks silently skipped)",
2424
"target_scan" : "Internal",

‎spoonmap.py‎

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6195,11 +6195,17 @@ def _filter_udp_live_hosts(output_path):
61956195
'honeypot_active_confirm': [
61966196
('__honeypot_active_confirm_note__',
61976197
'Optional. When true, SpooNMAP attempts a small number of raw TCP connects '
6198-
'against unscanned high ports on any host flagged as a suspected honeypot, '
6199-
'to confirm it. Default false, and absent means false: the tool sends no '
6200-
'traffic beyond the scan itself unless you turn this on. A confirmed host '
6201-
'answered a port that was never part of this scan, which is a strong tell '
6202-
'that it accepts every connection rather than running a real service.'),
6198+
'against unscanned high ports on any host flagged by the TTL-spread or '
6199+
'port-profile signals, to confirm it. Hosts flagged only by the tarpit '
6200+
'open-port-ratio heuristic are not probed. Default false, and absent means '
6201+
'false: the tool sends no traffic beyond the scan itself unless you turn '
6202+
'this on. A confirmed host answered a port that was never part of this '
6203+
'scan, which is a strong tell that it accepts every connection rather than '
6204+
'running a real service. Limitations: hosts outside target_file are never '
6205+
'probed; on a Full (1-65535) scan there are almost no unscanned ports left '
6206+
'to probe, so it does little or nothing; and honeypot detection as a whole '
6207+
'only runs on the masscan path, not the direct-nmap path used for scans '
6208+
'below nmap_threshold.'),
62036209
],
62046210
'target_scan': [
62056211
('__target_scan_choices__',

0 commit comments

Comments
 (0)