v0.2.0 — the hour after a breach
The kit was born from a breach and, until now, had nothing for the hour after one. Repo-wide grep before this release: "revoke" 0 hits, "forwarding rule" 0, "deploy key" 0, "reinstall" 0.
If you are in an incident right now
./panic.sh --short # the ordered checklist, phone-sized, no network needed
./panic.sh --triage # ...plus live "did anything get root?" probes
./preserve.sh # capture evidence BEFORE you start cleaning upOr open INCIDENT.md on your phone.
The ordering is the deliverable. Two steps are commonly inverted, and both inversions waste the whole effort:
- Crypto first. It is the only irreversible loss. A seed phrase is the wallet — "change the password" does nothing.
- Kill sessions before changing passwords. A stolen cookie authenticates without your password and without your 2FA, and a password change does not reliably invalidate sessions that are already live. Reset first and stop there, and the attacker is still signed in — behind a new password that now protects nothing.
Then: revoke OAuth grants (they survive every password change) → passwords, email first → the mail-persistence sweep (forwarding, filters matching reset/verify/code, send-as aliases, delegated access, app-specific passwords, recovery address and phone — all invisible in normal use, all surviving a reset) → developer tokens in blast-radius order, npm/PyPI publish tokens first, because that is where a personal breach becomes a supply-chain breach.
New in ExposureScan
scan_dev_credentials — the highest-pivot files on a solo dev's Mac, previously unscanned. SSH keys (plaintext vs encrypted, read from the header only), 11 credential files by key name and mode, shell-history token counts by line number, per-profile cookie counts, crypto wallet stores (20 extensions + 13 desktop bundles, unconditional P0), and Firefox login counts so the report is not silently Chrome-shaped.
--tcc — the grant inventory the README's central argument always implied and never delivered. It has always said malware inherits the grants of the trusted binary it runs inside; now it tells you which binaries those are. P0 for terminals, shells, SSH wrappers and bare interpreters holding Full Disk Access, Accessibility or Screen Recording — those are grant-inheritance vehicles, not apps. Run it. The answer is usually uncomfortable.
Fixed: WatchPost could be blinded by one rm
Deleting baseline.json was treated as a first run — the next run printed "No diffing on first run" and silently absorbed whatever had just been planted as legitimate. An .armed marker now makes deletion an alertable event. Editing the baseline directly, to pre-seed an entry so a later plant diffs as already-known, is caught by an HMAC tag. The baseline is 0600 in a 0700 directory; it was 0644, and it enumerates every persistence entry on the machine.
What was deliberately not built
preserve.shdefers entirely to Jamf Aftermath when installed. Free, Swift, purpose-built, collects a superset. Reimplementing it would be worse code doing a solved job.- No general hardening scanner. FileVault, firewall, update settings, sudoers, the CIS sweep — mSCP and Pareto Security own that and own it better. Only the narrow ClickFix-relevant slice ships.
Honest limits
- WatchPost's HMAC key sits beside the baseline under the same user, so anyone already running as you can forge it. This is tamper-evidence, not tamper-proofing. The root-owned variant that would be proof is named and not claimed.
- Safari login counts need a keychain prompt to obtain. The report names the surface and says why, rather than printing a number it cannot stand behind.
- The Verified badge on this tag only means GitHub matched the signature to a key on this account.
git verify-tagagainst the published fingerprint is the real check, because you control the allowed-signers file and GitHub does not.
Verifying
git verify-tag v0.2.0 # key: SHA256:ahS0yuup97TRBRmaRzk3iEbUlo/IK+VqXgd0sada2KU
./install.sh --verifyTests
78/78 detection corpus · 10/10 WatchPost baseline integrity · 4/4 pty integration · 72 ExposureScan (was 47). Six CI jobs, four of them on macOS runners. Both feature commits are independently green, so git bisect stays usable.