Repository navigation
Commit c7cf660
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
0 commit comments