We commissioned an adversarial review of our own monitoring tool before launch. Twenty-eight findings later, here is what it caught, what it could not break, and what shipped because of it.
Dormouse watches web pages and produces evidence that the watching happened: every check in a hash-chained trail, gaps surfaced instead of hidden, coverage as a verifiable query. A tool whose product is proof deserves a hostile reviewer, so before tagging v0.5.0 we pointed one at the codebase, then fifteen days and about seventeen thousand lines of Go old, with instructions to attack the claims: forge a valid trail, lose an alert without a trace, make the coverage report certify watching that never happened.
The review returned 28 findings. Nine were classed do-not-launch. Every finding below was traced in code or reproduced against the live binary before it was believed, and every fix landed with a regression test before the release was tagged.
The single most likely real-world coverage gap is dormouse itself not running. Two independent defects compounded so that exactly that gap left no evidence. First, the staleness watchdog's opening scan fired one full interval after startup, while the scheduler's first checks ran within seconds. A healthy source at restart healed the staleness anchors before the watchdog ever looked, so no alert fired no matter how long the daemon had been down. Second, the coverage report slid its reporting window forward to the first check it found inside the window, a courtesy meant for newly added targets that could not tell "added on day 26" from "down for days 1 through 25". Host down for three weeks, one restart, and the report read near-100 percent coverage.
The humbling detail: the codebase already contained the fix. The single-shot code path ran a pre-check watchdog scan, above a comment warning that a run waking after an outage must record the gap before its own checks heal the timestamps. The daemon path skipped it. The hazard we had named in writing was the one we shipped.
The fix is both halves: the daemon now scans synchronously before its first check, and the window slides only to the target's first check ever, so a long-watched target keeps its full window and an operator outage reads as the gap it is. A regression test seeds a long history, an empty window, and demands the report say so.
Alert delivery runs through a durable outbox: one row per target and field, upserted on change, drained by a delivery loop. The race: a change from 10 to 12 is queued as row seven. The drain begins sending 12 to a slow webhook. While the send is in flight, the page moves to 14 and the enqueue coalesces into the same row. The send succeeds, the drain deletes row seven, and the undelivered 14 goes with it. The diff baseline has advanced, so nothing ever re-detects it. The alert is not late. It is gone.
The fix is a generation column with compare-and-swap on every delivery-state write: a writer proves it still owns the row version it read, and a lost swap means a newer value owns the row and the write is abandoned. Alongside it we added a per-channel delivery ledger, because the audit also showed "delivered" was recorded on the first channel success: the console printing while the paging webhook exhausted its retries still counted as delivered. The ledger records every channel outcome, including abandonment, and the coverage report gained an ABANDONED column. An alert a channel never received is now a number an auditor can see, not a rotated log line.
Every check writes an audit entry in a deferred function, and the write reused the check's own context. On SIGTERM, in-flight fetches abort and, because the context was already canceled, their audit writes failed too. Reproduced live: a canceled check left zero rows in the trail. A fetch that went out on the wire, that the server saw, vanished from the record on every deploy and restart, and no verifier could flag the hole because the chain only covers rows that exist.
The fix is one line, context.WithoutCancel for the deferred write, and
its cousin: a recover handler that converts a panicking check into a recorded failed
check instead of a dead daemon. Both carry tests that kill and panic mid-check and then
demand the trail account for it.
Watch a client-rendered page with a plain fetcher and you receive a near-empty HTML shell. The old degenerate-capture guard only protected targets with selectors, and the quickstart's simplest form has none, so the shell became the baseline, every later check diffed shell against shell, and the target reported flawless coverage of nothing, with a perfect audit trail underneath it. The fix: a whole-page capture that yields almost no visible text now fails the check with an error that says the page likely renders with JavaScript, which dormouse does not execute. Refusing to pretend is the product.
The key had no floor. The trail stores every hashed payload beside its tag, so a human-chosen chain key was open to an offline dictionary attack at GPU speed, and recovering it means re-signing the whole trail. Keys under 32 random bytes are now refused at startup.
Trails could transplant. Nothing bound a chain to its database, so two instances sharing one key could swap trails and both would verify. Every entry now mixes a fixed domain tag and a per-database random installation id.
Field joins were ambiguous. Chain input joined fields with a separator byte that an attacker-influenced error string could itself contain, admitting multiple field splits with one hash. Fields are now length-prefixed, which ends the ambiguity instead of arguing with it.
Changes are stamped when observed but committed after judging and annotation, seconds later under load. The digest resumed from a wall-clock cursor, so a change stamped 06:59:58 and committed at 07:00:02 fell behind a cursor already advanced to 07:00:00, permanently. Anything consuming the API with a time cursor inherited the same trap. The fix: cursors are now commit-ordered row ids, kept per delivery channel, and the API exposes the id so consumers can cursor on something that cannot land in the past.
The review also attacked things that did not move, which is the half of an audit nobody publishes: the cumulative-diff-across-outage guarantee and the test that pins it, real HMAC-SHA256 with constant-time comparison on every path, no length-extension, the atomic observation-plus-changes commit, pinned-head verification that catches truncation even by whoever holds the database, and online backups. A stress run of 640 concurrent audit writes across 16 goroutines produced zero dropped writes and zero chain breaks. Knowing where the walls held is why we paid for the attack.
All 28 findings were fixed before v0.5.0 was tagged, each with a regression test.
The release gate now runs the full suite, the race detector, cross-platform builds
including Windows, a linter that fails the build, a live smoke test of the real binary,
and pack check, which live-fetches every curated vendor source and fails
the release if any is broken. The audit will repeat for future releases, because a tool
whose failure mode is confident silence has to be built by people actively trying to
make it fail.
The buyer-facing summary of this discipline lives at How Dormouse is tested, and the tamper-detection demo you can reason through yourself is at How the proof works.