Every "never phones home" line in a tool's README is a policy statement until you open the file that enforces it, and the enforcement is usually shorter than the promise. I cloned Zero Breach Toolkit on Oct 28 and read its network guard before running anything. Today's GitHub API reports the repo at 4,211 stars, 443 forks, created 2026-10-25T14:41:34Z, last pushed 2026-11-02T07:38:11Z, Apache-2.0, Python, 12,029 KB. The clone on my disk is 5.0 MB, scan/zbtriage/netguard.py runs to about ninety lines of actual code, and the guarantee itself lives there.
What the guard patches
The docstring sets the bar at "we make it impossible," meaning intent never reaches the code review stage. While a scan sits inside no_network(), every name-resolution and outbound-connection entry point in the socket module raises NetworkContactAttempt. Two tuples hold the list.
_BLOCKED names six functions on the module, getaddrinfo, gethostbyname, gethostbyname_ex, gethostbyaddr, getnameinfo, create_connection. _SOCK_METHODS covers six more, this time on the socket object itself, connect, connect_ex, sendto, sendmsg, sendall, send. Twelve patched entry points total, swapped in with setattr when the context manager opens and restored when it closes.
The comment above the second tuple explains why the list reaches six methods, "sendto and sendmsg matter because a UDP datagram needs no prior connect(): without them a bug could still push bytes to a resolver or a C&C over UDP." A scanner that blocks only socket.socket.connect leaves an unpatched path to a server, the misspelled IOC gets shipped, the vendor's log sees it. I read those twelve names against my own grep for urllib, requests, and subprocess in the runtime modules, then ran --describe on the clone and got schema_version 1.6, tool version 0.1.8, exit 0.
The gap the same file admits
Right under that promise, the docstring prints, "This does NOT block subprocess (OS pipes, not Python sockets)". The host module uses that hole deliberately, reading the local DNS resolver cache through PowerShell so an already-cached entry resolves nothing. Phoneguide.py spends it on adb kill-server plus one local exe under owner consent.
Naming the hole carries more weight than the twelve names do, it tells you which layer the claim actually covers. The guard reaches Python sockets, it says so, it adds that it is not an OS network sandbox. Nothing about a Go binary, a shell helper, or a dependency reaching the network through C. For this repo that trade is fine because the scan path is pure Python, the vendored indicator bundles are text files, feeds.py closes with "an indicator is never resolved or contacted: these are text files." A different tool claiming offline while shelling out to curl would carry the identical patch list and mean something else entirely.
Where the verdict vocabulary earns its keep
The second guarantee lives in result.py, and it decides whether a report can be trusted at all. Status values are CLEAN, HITS, UNKNOWN, PARTIAL, NOT_APPLICABLE. Doctrine D3 above them reads, "A module reports CLEAN only for checks that actually ran and found nothing. A check that could not run is UNKNOWN, which is a DISTINCT outcome from CLEAN. Never collapse the two."
Exit codes carry the same split, and 0 means no actionable matches AND every applicable check in scope ran, 1 is a setup or output error, 2 is matches worth reading, 3 is no matches but incomplete checks, and the contract prints "This is NOT the same as exit code 0: nothing was cleared where checks did not run." 4 covers someone leaving the start menu without scanning.
Running --scan-type default --preview on my Mac today returned host_platform darwin, scanned false, and four checks, browser, hash, host, unix-host. The limits print alongside them, file hashing stays bounded to 120 MiB per file, coverage is selected startup config only on this platform, the scope line reads "Full means broader checks in the usual roots, not every drive or every user", and the first limit is "Read-only local evidence; no infection-free guarantee." A clean run there clears four check families inside one declared scope, and the tool prints its own summary line as "A clean result is not a clean bill of health."
Indicator provenance, measured
The scanner ships its evidence on disk and fetching happens only through --update. dist/adfection.misp.json is 273,514 bytes, dist/adfection.stix.json is 668,472 bytes (811 objects), iocs/adfection_iocs.txt is 35,548 bytes over 600 lines with a .sha256 companion whose value I recomputed locally as 42bff7164687f1faf6ab5691738e156f72684cadbf63e403150e02acc65ad518, curated_mercenary.txt is 4,353 bytes over 58 lines.
Updating feeds is the one explicitly invoked network op, constrained in code by an allow-list of four owners (mvt-project/mvt-indicators, AmnestyTech/investigations, AssoEchap/stalkerware-indicators, citizenlab/malware-indicators), FEED_PINS holding a reviewed commit SHA per source repo, FEED_FILE_SHA256 recording a hash per file at that commit, MAX_DOWNLOAD = 64 * 1024 * 1024 with the read overshooting by one byte so an oversized body fails before parsing. A failed download keeps the previous good copy. The cached manifest hash re-checks on load, and the comment states its limit, "That detects accidental corruption; it cannot stop someone able to edit both the file and the manifest."
Reports sanitize indicators too, http:// becomes hxxp://, ftp:// becomes fxp://, so a hit list can't be pasted into a browser and reach the lab.
How I'd audit another tool like this
Read order matters more than speed here. netguard.py first, count the patched names against your own grep for egress paths. Second, the status vocabulary and exit contract, because a scanner printing CLEAN where checks were skipped is a different product from one printing UNKNOWN. Third the evidence ladder, fifteen grades in METHODOLOGY.md running on-disk, on-device, file-pattern, persistence, ran-command, execution-record, inventory-record down through contacted, device-log, received, dns-record, attempted, configured, referenced, with the rule that grade decides advice and match count does not. Fourth the release verification page, which offers two independent checks, a GPG signature against ArchiveGenocide's Ed25519 key (fingerprint C24E C92B 12D6 670A 2516 065F 9B4D 5754 99AF A53C) plus a build you reproduce yourself.
Issue history shows the review loop working, elias-ai-ai filed six issues on Oct 28 covering IPv6 canonicalisation, pinned feed commits with SHA-256, temp-file cleanup, crafted-evidence hang, OneDrive placeholders, clean-tree CI rebuild, all fixed and closed same day, four open now. The README credits "Made by archivegenocide. Built on Zero Breach."
I stopped before the host scan itself. Scope decides which files get read and the report lands next to them, and cloning gave me the code without giving permission to sweep my own disk at scale.
Which version of that offline claim would you accept for a tool written in Go or shipped as a binary, where no setattr equivalent exists, and what code would prove it?

Top comments (0)