From 2d9d896007e8dedca9147a5445c306d4e55968a2 Mon Sep 17 00:00:00 2001 From: Justin Bollinger Date: Tue, 25 Aug 2026 13:23:09 -0400 Subject: [PATCH] fix: disclose Full-scan rate cap instead of clamping silently MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A Full scan clamps masscan to full_scan_rate (10000 pps External, 1000 Internal) so a single 1-65535 sweep cannot exhaust firewall state tables. The clamp was silent, while main()'s run summary echoes the operator's requested max_rate — so requesting 5000 pps on an Internal Full scan printed "Max Packet Rate (pps): 5000" and then scanned at 1000, with the cap documented only in the config-help text an interactive run never shows. That reads as max_rate having been ignored outright. Print a notice when the clamp actually lowers the rate, naming both rates, why the cap exists, and that targeted scans use the full rate. Guarded on the comparison so a rate already under the cap stays quiet, and placed inside the scan_type == 'Full' branch so batched scans — which are deliberately uncapped — never mention it. Co-Authored-By: Claude Fable 5 --- CLAUDE.md | 2 +- spoonmap.py | 11 +++++++++++ tests/test_spoonmap.py | 23 +++++++++++++++++++++++ 3 files changed, 35 insertions(+), 1 deletion(-) diff --git a/CLAUDE.md b/CLAUDE.md index ec07c11..09525f2 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -175,5 +175,5 @@ Internal discovery runs a single masscan sweep (no source-port override) followe - **Hostname support**: hostnames in the target file are resolved once at startup; nmap receives the original hostname (for SNI/vhost), masscan receives the resolved IP - **IPv4-only, enforced at the edges**: the tool scans IPv4 exclusively (masscan/nmap invocations, target expansion, and address sorting all assume it). IPv6 is rejected rather than half-supported, in two places. (1) `_build_discovery_target_file()`'s `_parse_ranges()` skips any entry `ipaddress.ip_network()` resolves to a non-v4 network and prints the offending file, line number, and content — previously the v6 bounds were stored silently and only surfaced hundreds of lines later as `AddressValueError: ... (>= 2**32)` from `summarize_address_range()`, and only when an exclusions file happened to be configured. (2) The masscan/discovery XML parsers (`_parse_masscan_ping_xml()`, `_parse_nmap_sn_xml()`, `_run_masscan_batch()`) select `address[@addrtype='ipv4']` instead of the first `
` child, matching what the nmap-side parsers already did, so a dual-stacked host's IPv6 or MAC string can't enter `live_ips`/`port_ips` and become a masscan `-iL` target. Address sorting goes through `_ip_sort_key()`, which orders valid IPv4 numerically and sorts anything unparseable last instead of raising — the three former inline `tuple(int(o) for o in x.split('.'))` keys ran *after* a completed sweep, so one odd entry discarded the whole thing. - **XML result parsing is per-element defensive**: every `etree.parse()` site guards the *walk* as well as the parse. Attributes are read with `.attrib.get(...)` and the element is skipped when the identifier is missing — never a bare `attrib['addr']` or `findall('address')[0]`, both of which raise `KeyError`/`IndexError` that `except etree.ParseError` does not catch. Those exceptions escaped the guard and discarded the results for *every other host* in the file (or, in `_host_elem_to_dict()`, lost `spoonmap_output.xml`/`.json` for the whole run) over one truncated element. `