Skip to content

Commit c7cf660

Browse files
bandrelclaude
andcommitted
fix: cover the exclusions file in every resume gate
No resume gate accounted for the exclusions file. --excludefile is passed straight to masscan and nmap and never enters the -iL file the coverage record is built from, so narrowing exclusions.txt between runs widened the real scan with nothing on disk to notice it: every cached phase still satisfied its gate and the newly in-scope hosts were never scanned. Removing an entry from exclusions.txt mid-engagement is routine — a host is cleared for testing, or an over-broad exclusion is narrowed once the client confirms scope. On the next --resume, resolved_targets.txt is unchanged, the -iL files are unchanged, every coverage record still matches, and the run reports the engagement as complete while silently omitting the hosts the operator deliberately brought into scope. A phase's effective coverage is `targets - exclusions`, so the two halves are compared in opposite directions: - targets: accept when current is a subset of the record (unchanged). - exclusions: accept when current is a superset. Excluding more means a strictly smaller scan set the cache still covers; excluding fewer means something previously skipped is now in scope and must be re-scanned. _host_discovery() stops passing target_file=None. Its mtime baseline on resolved_targets.txt does cover the targets half on its own, but its real target is the exclusion-subtracted filtered_target, so exclusions changes were invisible to it too. It records against target_file and exclusions_file rather than filtered_target, because those are what the next run can re-derive. Both halves now live in ONE json sidecar, <output>.coverage, written by a single _atomic_write. As two files written in sequence, a KeyboardInterrupt between them — routine during a scan, and a BaseException that `except Exception` does not catch — left a fresh target list beside a stale exclusion list, and the gate accepted that pair as an exclusion-free scan. Demonstrated before fixing; now a test. One file makes "both halves describe the same run" a filesystem guarantee instead of a comment, and lets an empty exclusion set be recorded explicitly rather than encoded as an empty file. Each phase also drops its record BEFORE it scans, not only on failure. A run killed outright never reaches the stamp, so the previous run's record would otherwise sit beside output that run had already replaced. The hazard is specifically a record covering MORE than the output now on disk, since a subset test accepts it; a narrower leftover only costs a redundant re-scan. An interrupt during the write also discards before re-raising, because _atomic_write leaves the prior record in place if it never reaches its rename. target_file and exclusions_file are keyword-only with no default. That is exactly as mandatory as positional — Python raises TypeError either way — while making the call sites say exclusions_file=None instead of trailing a bare None, which was the most consequential and least legible argument in the nmap calls. target_file loses its None branch entirely: no phase lacks a target, and keeping the escape hatch meant a future caller could pass None with a real exclusions file and silently lose both checks. Pinning both directions was necessary and neither alone sufficient. Dropping exclusions at the gate only makes it stricter, so it is caught by the "unchanged exclusions still resume" cases; dropping them at the record makes it accept a narrowed file, caught by the "narrowed re-scans" cases. The per-phase tests therefore drive the real _run_masscan_batch()/nmap paths with only subprocess.Popen faked — an earlier version stamped inside its own fake and passed against every mutation, proving nothing. Also replaces a vacuous assertion that checked a leftover NARROWER record was rejected: the targets check rejects that regardless, so it never tested the discard. It now stamps wide, fails the narrow run's write, and asserts rejection. Every wiring point was confirmed pinned by mutation in a scratch copy. Closes #48 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent dfa2809 commit c7cf660

3 files changed

Lines changed: 803 additions & 208 deletions

File tree

0 commit comments

Comments
 (0)