feat: v0.2.0 — incident response, dev-credential surface, TCC inventory, baseline integrity - #2
Merged
Merged
Conversation
…e integrity The kit was born from a breach and had nothing for the hour after one. Repo-wide grep before this commit: "revoke" 0 hits, "forwarding rule" 0, "deploy key" 0, "reinstall" 0. canary/README.md ended at "you find out" and handed off to no procedure — a user whose decoy fires at 2am was holding an alert with no next step, on the compromised machine. INCIDENT.md — the ordering IS the deliverable - Crypto first, because it is the only irreversible loss. A seed phrase is the wallet; "change the password" does nothing. - Kill sessions BEFORE changing passwords. This is the step people invert. A stolen cookie authenticates without the password and without 2FA, and a password change does not always invalidate live sessions — so resetting first and stopping there leaves the attacker logged in behind a new password. exposurescan.py already said cookies "bypass password + MFA" and the kit never turned that insight into a procedure. - Revoke OAuth grants (survive every password change), then passwords (email first), then 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 survive a reset. - Developer tokens in blast-radius order: npm/PyPI publish tokens FIRST (a personal breach becomes a supply-chain breach), cloud keys disable-then-delete so the audit trail survives, then PATs, SSH/GPG, and per-repo deploy keys + Actions secrets — the most forgotten items, because there is no revoke-all. - The reinstall decision as a bright line, not a vibe: did anything get root? Written to be worked from a phone, in order, during a bad morning. panic.sh — prints it with no network, no browser, no dependencies - --short fits on a phone screen; --paper pipes to lpr; --triage adds read-only probes that answer step 8 (LaunchDaemons, PrivilegedHelperTools, config profiles, system extensions, sudoers.d, authorized_keys, sshd/screensharing, SIP) with facts instead of memory. - The ordered summary is hard-coded rather than parsed out of INCIDENT.md: if the repo is damaged, the ORDER is the part you cannot afford to lose. preserve.sh — capture before you clean up - Defers to Jamf Aftermath entirely when installed. It is free, Swift, purpose-built and collects a superset; reimplementing it would be worse code doing a solved job. The value here is the ClickFix-specific ordering. - Built-in collector otherwise: 29 artifacts incl. kMDItemWhereFroms origin URLs for recent downloads (what tells you which site delivered it), sha256 + codesign verdict per persistence plist, and TCC grants. Sealed read-only with a MANIFEST. - Unified log filtered on `process`, not `eventMessage CONTAINS`. The latter forces a full log-store scan that runs for minutes — long enough that during a real incident you kill it and lose the artifact entirely. WatchPost baseline can no longer be blinded - Deleting baseline.json was read as a first run: the next run printed "No diffing on first run" and silently absorbed whatever had just been planted. An .armed marker now makes deletion an alertable event that refuses to re-baseline without an explicit --init. - Editing the baseline directly (pre-seeding the attacker's future entry so the real plant diffs as already-known) is caught by an HMAC tag. - Baseline is 0600 in a 0700 dir; it was 0644 and enumerates every persistence entry on the machine. - README states the honest limit: the key sits next to the baseline under the same user, so this is tamper-evidence, not tamper-proofing. The root-owned variant is named and explicitly not claimed. - --no-update documented as THE incident flag; a normal run promotes the baseline and erases the diff that proved something appeared. tests/test-watchpost-baseline.sh — 10 checks covering all three attacks, plus the one that matters most: a genuinely new plant is STILL reported after all the hardening. New macOS CI job. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AcTJUv94F34MdtGZCGyyur
Two additions from docs/v0.2.0-plan.md, both names-and-shape-only, both held to the v0.1.1 invariant: no secret value reaches any artifact. scan_dev_credentials — the highest-pivot files on a solo dev's Mac, previously unscanned. An SSH key plus a GitHub token is push access to every repo you own, which makes a personal breach a supply-chain breach, and the report said nothing about any of it. - SSH keys: filename + type + ENCRYPTED vs PLAINTEXT. Plaintext is P0 with that pivot spelled out. Encryption is read from the HEADER ONLY, bounded to 112 base64 chars (84 bytes) — short of the private section in both the PEM and openssh-key-v1 layouts. For encrypted keys the type comes from the .pub half rather than widening the bound. An unparseable header reports as ENCRYPTED: unknown must never render as the P0 case. - 11 credential files (~/.aws/credentials, .npmrc, .pypirc, .netrc, gh/hosts.yml, docker/config.json, .kube/config, gcloud, .git-credentials, .cargo/credentials.toml, .gem/credentials): presence, mode, KEY NAMES only. World/group-readable escalates to P0. .git-credentials and .netrc emit counts and hostnames only, because every line of them IS the credential. - Shell history: counts + line numbers + the credential prefix CLASS, never the text. Pasted secrets routinely land in ~/.zsh_history and nothing surfaced it. - Cookie row counts per profile, carrying the "revoke sessions BEFORE rotating passwords" pivot that INCIDENT.md step 2 depends on. - Crypto wallets: 20 extension IDs + 13 desktop bundles, unconditional P0. The tool tiered seed phrases P0 in Notes and never looked where wallets live. - Firefox login + cookie counts, so the report is not silently Chrome-shaped. --tcc — the README's central argument, finally actioned. It has always said malware inherits the grants of the trusted binary it runs inside, and never told you which binaries those are. Reads both TCC.dbs through the existing safe-copy path, handles modern auth_value and legacy allowed schemas, degrades to a note without Full Disk Access. P0 for terminals, shells, SSH wrappers, remote-access tools and bare interpreters holding FDA/Accessibility/ScreenCapture/ListenEvent/ PostEvent/DeveloperTool — those are grant-inheritance vehicles, not apps — said in plain language: anything you run inside Terminal gets Screen Recording too. Bundled: Secure Keyboard Entry state, sshd/screensharing, Secure Boot level. Deliberately NOT built: FileVault, firewall, update settings, sudoers, password policy, the CIS sweep. mSCP and Pareto own that and own it better; they are cited in the README instead. This is the narrow ClickFix-relevant slice only. Honest limits, documented rather than papered over: - Safari login counts are not obtainable without a keychain prompt. The report names the surface and says why instead of printing a fabricated number. `security dump-keychain` stays opt-in behind EXPOSURESCAN_KEYCHAIN_COUNT=1. - redact()'s proximity rule over-fires on labels containing a bare auth/token/ credential/cookie, and on filenames >=20 chars. Reworded the tool's own strings rather than weakening the rule, per the project's stated policy, and documented the residual over-fire. tests: 47 -> 72. New tests/make-sample-report.py regenerates sample-report.md from the REAL scanner against a synthetic $HOME, with --check failing when stale, so "generated, not hand-written" is enforced rather than asserted. Leak tests grep the rendered markdown AND the JSON sidecar for the fixture's key bodies and every fake token, asserting zero hits. Verified against this machine: the tool's 64 TCC grants matched a hand-run sqlite3 over both databases exactly — none invented, none missed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AcTJUv94F34MdtGZCGyyur
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AcTJUv94F34MdtGZCGyyur
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The kit was born from a breach and, until this release, had nothing for the hour after one. Repo-wide grep before this branch: "revoke" 0 hits, "forwarding rule" 0, "deploy key" 0, "reinstall" 0.
canary/README.mdended at "you find out" and handed off to no procedure — a user whose decoy fires at 2am was holding an alert with no next step, on the compromised machine.Human
Run
--tccon your own machine. It will tell you which binaries already hold Full Disk Access, Accessibility and Screen Recording — the grants a pasted payload inherits for free.Agent
Intent. Close the three gaps the v0.2.0 plan ranked highest by real-victim impact, and nothing else. No new tools; the kit stays at six.
The ordering in
INCIDENT.mdis the deliverable, not the prose. Two steps are commonly inverted and both inversions waste the entire effort:exposurescan.pyalready said cookies "bypass password + MFA"; the kit never turned that into a procedure.Developer tokens are ordered by blast radius — npm/PyPI publish tokens first, because that is where a personal breach becomes a supply-chain breach; cloud keys disable-then-delete so the audit trail survives deletion.
WatchPost could be blinded by one
rm. Deletingbaseline.jsonwas read as a first run: the next run printed "No diffing on first run" and silently absorbed whatever had just been planted as legitimate. An.armedmarker makes deletion alertable; an HMAC tag catches pre-seeding the file so a later plant diffs as already-known; the baseline is 0600 in a 0700 dir (it was 0644, and it enumerates every persistence entry on the machine). Test C in the new suite is the one I care about most: a genuinely new plant is still reported after all the hardening — it is easy to lock a monitor down until it stops monitoring.What was deliberately NOT built, and why, because the roadmap's credibility depends on this section being unflattering:
preserve.shdefers wholly to Jamf Aftermath when installed and stops. Free, Swift, purpose-built, collects a superset. The value here is the ClickFix-specific ordering, not the collection.--tcc) ships, and the README cites them for the rest.Honest limits documented rather than papered over:
/var/db/watchpostvariant that would be proof is named and explicitly not claimed.redact()'s proximity rule over-fires on labels containing a bareauth/token/cookie, and on filenames ≥20 chars. The tool's own strings were reworded rather than weakening the rule, per the project's stated policy; the residual over-fire is documented.Edge cases. SSH key encryption is read from the header only, bounded to 112 base64 chars — short of the private section in both the PEM and
openssh-key-v1layouts. An unparseable header reports as ENCRYPTED, because unknown must never render as the P0 case.preserve.sh's unified-log query filters onprocessrather thaneventMessage CONTAINS, which forces a full log-store scan that runs for minutes — long enough that during a real incident you kill it and lose the artifact entirely.Verification. ExposureScan's 64 TCC grants matched a hand-run
sqlite3over both databases exactly — none invented, none missed. Leak tests grep the rendered markdown and the JSON sidecar for fixture key bodies and every fake token, asserting zero hits. Both feature commits are independently green, sogit bisectstays usable.🤖 Generated with Claude Code
https://claude.ai/code/session_01AcTJUv94F34MdtGZCGyyur