What does a clean scan actually prove?
Most verification tools ship two answers, something found and nothing found, and leave the reader to split the difference between "I checked everywhere and the evidence bag is empty" and "half my checks never ran." That
Most verification tools ship two answers, something found and nothing found, and leave the reader to split the difference between "I checked everywhere and the evidence bag is empty" and "half my checks never ran." That collapse is where security theater lives, because an idle scanner and a working one print the same green line. The toolkit I audited on October 28 refuses that vocabulary outright. Its own summary field reads "A clean result is not a clean bill of health," and five exit codes are the contract before anyone runs a single check.
Reading the contract back from the tool
I cloned interplains/Zero-Breach-Toolkit (created 2026-10-25, Python, Apache-2.0, it stood at 2,408 stars when I pulled it), read the runtime modules, then asked the tool directly about its verdicts. One call, no arguments past --describe, returned schema_version 1.6, tool version 0.1.8, and this table verbatim:
0 No actionable indicator matches were found, and every applicable
check in the selected scope ran. ... Still not a clean bill of health.
1 A setup or output error occurred.
2 Indicator matches beyond saved references were found.
3 No actionable indicator matches were found, but checks or indicator
sources were incomplete. This is NOT the same as exit code 0.
4 The person left the start menu without running a check.
Exit 0 means "nothing found AND everything ran," a conjunction you cannot fake by accident. Exit 3 means an empty bag where something could not run, and the tool spells out that those are different answers. Exit 4 exists because a human abandoning the menu deserves its own state on a shared machine, the alternative is someone else reading yesterday's report and trusting it.
Ever watch a CI bot go green on a skipped step? Same lie, bigger surface. The question your scanner should never get to answer is "clean" without attaching what scope produced it.
The mechanism under the vocabulary
The network guard is the interesting half. scan/zbtriage/netguard.py monkey-patches socket.getaddrinfo, gethostbyname, gethostbyname_ex, getnameinfo, create_connection, plus connect, sendto, sendmsg and friends on the socket object itself, every one raising NetworkContactAttempt on live contact attempts. A scan that promises "never contacts an indicator" gets enforcement patched inside the process. And the module comment admits subprocess is NOT blocked, needed for a local PowerShell read of the DNS resolver cache, and says plainly this is not an OS-level sandbox. They enforce their promise where they can and name the hole where they can't. That combination is rare enough that I'd take it as a quality signal in any repo.
Indicator feeds carry the other honest trick. --update is the only network operation in the tool, it downloads from an allow-list of official publisher sources pinned at the commit recorded for each release, requires a SHA-256 per file, caches those hashes, and states that publishers do not sign their sources. Pinning plus hashing is therefore the strongest binding available, a weaker claim would have said "we trust the feed."
Then grading. Matches arrive on an evidence ladder whose rungs run from an artifact actually on disk, down through on-device traces, file patterns, persistence and command records, contacted-network and device-log entries, dns records, attempted contact, configured hooks, all the way to a bare reference sitting in some saved page. A referenced name and an on-disk binary are both matches, they are not both emergencies, and the advice attaches to the grade while the count stays decorative. The report also sanitizes indicator URLs to hxxp:// form and refuses to treat saved-page references as contact attempts, which stops the report itself from becoming a trail someone forwards.
Where the honesty has edges
Nothing here changes the verdict machinery, these are gaps the audit found around it, and one was still open when I pulled.
A review loop ran the same day the repo went live: issues #7 through #12 filed and fixed in one pass, covering IPv6 canonicalization, pinned feed commits with SHA-256 checks, temp-file cleanup, a crafted-evidence hang, OneDrive placeholder files, and a clean-tree CI rebuild. Issue #15 stayed open at pull time, "there is no report.txt" after a Windows run, so the five-code contract can be honored by the process and still leave the human holding nothing. A tool that exits 0 while its report failed to save has quietly demoted itself to the old two-state world for everyone reading along.
Two design decisions worth keeping either way. Doctrine D3 says a check that could not run is a gap, never a clean result, exactly what separates exit 0 from exit 3. Doctrine D4 parks hunches under a "worth a look" heading with the usual harmless explanation attached, and they never move the verdict. Speculation gets a shelf, findings keep the floor.
What shipped on my machine
I stopped at the preview because the full host scan takes about fifteen minutes and the scope question decides whether any verdict means something. The preview call, --scan-type default --preview, printed the bounds before committing to work:
{
"scanned": false,
"host_platform": "darwin",
"limits": [
"Read-only local evidence; no infection-free guarantee.",
"File hashing remains bounded to 120 MiB per file.",
"Full means broader checks in the usual roots, not every drive or every user."
],
"checks": ["browser", "hash", "host", "unix-host"]
}
Four check families offered, hashing bounded to 120 MiB per file, and scanned: false sitting right there so a screenshot of this JSON can't launder itself into an all-clear. The phone lane found nothing to check, no backups present under the standard MobileSync path on this machine, so clearing your iPhone with this tool means making an encrypted local backup first, then running against that.
Three rules your own verifier should copy
- Give "couldn't check" its own exit code, and write onto the report which scope produced it. Two states cannot carry three truths, found-and-clean and empty-but-skipped and nothing-ran are different sentences.
- Bound the work and print the bound before it runs, the 120 MiB hash cap and the skipped-drives line both arrive as promises you can diff the run against.
- Enforce the no-contact claim inside the process where you can patch it, name the hole where you can't, subprocess escape hatches belong in a comment nobody has to read.
Repo math for the record: 794 KB at pull time, HEAD c410605 from 2026-10-28 13:53, indicator bundles vendored in dist/ (a 273 KB MISP file, a 668 KB STIX one, host and domain lists), so feeds refresh only when you explicitly ask.
What does your scanner's zero promise right now, and can you tell from its output alone? If you've written verdict vocabularies for one-shot checks that must stay honest about skipped work, show me yours in the comments, receipts beat screenshots here.
Originally published by Dev.to Security. Aggregated on AIWithGhost for educational purposes β full credit and traffic to the original publisher.

