Consolidate all branches onto main, fix pre-existing failures, and implement the missing planned modules - #4
Merged
Conversation
…rage Adds the detection and remediation content for a household whose Wi-Fi has been broken into, and for the cryptojacking that tends to arrive with it. New modules: - crypto_miner_persistence (macOS/Windows/Linux) finds what restarts a miner after it is killed: launch items, cron, systemd units, Run keys, scheduled tasks, shell profiles, and dropped miner configs. Wallet addresses and stratum arguments are treated as high-confidence evidence; a product name alone is not. - win_crypto_miner_detect gives Windows the live-miner detection that previously existed only for macOS: process command lines, established connections to mining-pool ports, and high CPU as a low-confidence lead. - browser_cryptojacking_check covers in-browser mining, which never shows up in a process list: extension code, startup pages, and hosts-file entries. Blackholed mining domains are recognised as protection, not as a threat. - lan_device_inventory lists the devices actually visible on the network, with OUI vendor labels, and says plainly that a quiet device can be missing. - arp_spoof_check looks for traffic interception: the gateway's address answering for other hosts, duplicate addresses, and multiple default routes. - router_security_audit checks the gateway's exposed admin surface and UPnP, and carries the full router reclaim procedure. - stalkerware_scan separates covert monitoring software from workplace and parental products and from dual-use remote access, and leads its guidance with safety planning rather than removal instructions. Supporting content: shared cryptojacking IOC data, a shared neighbour-table helper, expanded known-bloatware datasets (adware, scareware, bundled miners, OEM trials), and the home_network_intrusion profile and five-phase guide, ordered so the network is reclaimed before the devices are cleaned. rescue.runtime gains load_content_module() so modules can share a helper by path, which works in a source checkout, a pip install, and a frozen bundle. Every fix() here is guidance only, marked ActionKind.GUIDANCE, and every external command and filesystem scan is bounded by a timeout or a cap. Also fixes three pre-existing Python 3.12-only f-strings that made win_malware_indicators and win_bsod_analysis fail to load, and test_module_win_scheduled_tasks_security fail to collect, on the declared minimum Python 3.11. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AsDpjoDi59AXxhoy8TriUQ
Adds a six-phase guide that doubles as a recovery checklist — `rescue guide identity_theft_recovery` prints the outstanding steps and `--complete <n>` marks them off, so progress survives across the weeks this actually takes. The phases are ordered the way recovery works rather than the way it feels: 0. The first hour — start a recovery log, write down what is known, and deal with money that has already moved (reporting windows for unauthorised debit charges are short). 1. Confirm the device is not the leak — the only automatable phase, because changing passwords on a machine with a keylogger hands them straight back. Also covers email forwarding rules and carrier port-out locks, which both survive a password change. 2. Freeze — all three credit bureaus plus Innovis, NCTUE, and ChexSystems, which is where phone contracts and bank accounts get opened. 3. Report — IdentityTheft.gov first, because the FTC identity theft report is what unlocks the §605B block and the seven-year extended alert. Then police, IRS/state tax, SSA, CFPB, and the channels specific to SIM swap, medical, criminal, child, and deceased-relative identity theft. 4. Dispute — the distinction between a dispute (30 days, contestable) and an identity theft block (four business days, backed by the FTC report), plus the follow-up schedule for accounts that reappear via a new collector. 5. Monitor and rebuild — the long tail, and what recurrence looks like. The profile pairs the guide with the twelve existing modules that answer the one technical question here: is this device leaking credentials. It claims no automation for anything that happens at a bank or a government agency. US-first, with equivalents for the UK, Canada, Australia, and the EU in phase 2, and free case-managed support services named in phase 5. Also regenerates the integrity manifest for the runtime.py change in the previous commit, which was making every launch print a tamper warning. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AsDpjoDi59AXxhoy8TriUQ
Both guides had helplines scattered inline across steps, which is where they are least findable — somebody who needs a number six weeks from now is not going to re-read phase 3 step 4 to find it. Each guide now ends with a dedicated reference phase. identity_theft_recovery phase 6 covers free case-managed help (Identity Theft Resource Center, the AARP helpline, the DOJ elder fraud hotline, IDCARE, the Access Now digital security helpline), the official reporting channels, all six credit bureaus and databases that accept freezes, abuse-specific support including coerced debt, free legal aid, and non-US equivalents. home_network_intrusion phase 5 covers abuse support first — because when the intruder is someone known, the safety plan changes the order of the technical work — then free incident response, where to report an intrusion, a pointer to the identity theft guide when accounts are involved, and how to tell whether a router is worth keeping. Both open with the same warning: search results for support numbers are bought by scam call centres, and someone mid-recovery is exactly who they want. Type the domain, use the number on the card, and nothing legitimate is ever paid for in gift cards. That warning is worthless below the list, so it is step 1. stalkerware_scan's safety guidance gains the same country-by-country numbers, plus a note not to look them up from the monitored device — browsing history is one of the things that gets reported. A test asserts the scam-helpline warning stays first in the resources phase, and the step-title check caught one heading too vague to work as a checklist item. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AsDpjoDi59AXxhoy8TriUQ
The 'open-setting' channel passed any renderer-supplied string straight to shell.openExternal, which hands the URL to the OS handler. The renderer escapes all scan output today, so there is no live injection path — but the URLs it opens come from permissions-content.json, a data file the content-update mechanism is designed to replace. An unrestricted channel therefore turns "attacker influences shipped content" into "attacker launches local files and registered protocol handlers", which is a poor trade for a tool people run on machines they already suspect are compromised. The walkthrough only ever needs OS settings panes, so the check is an allowlist of scheme prefixes rather than sanitisation — allowlists do not have parser differentials. Matching is startsWith, so a permitted scheme embedded later in a file:// URL does not qualify it. Also sets sandbox: true, denies window.open, and blocks navigation: the renderer only ever loads the bundled local page and has no reason to do any of the three. The predicate lives in its own module so it is testable without booting Electron, and package.json gains the "test" script the desktop app was missing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013g8dGcNgzJ1vZCw6kguGEu
pyproject declares requires-python = ">=3.11", but 30 call sites across 16 modules called Path.is_file(follow_symlinks=False) / Path.is_dir(...). That keyword argument reached pathlib in 3.13, so on 3.11 and 3.12 every one of these raises TypeError at runtime — these modules did not work at all on two of the three supported versions, and roughly 45 tests were failing because of it. rescue.fsbounds gains is_file_nofollow / is_dir_nofollow, which get the same no-follow semantics from os.lstat on every supported version and return False rather than raising on a missing or unreadable path, matching how Path.is_file swallows OSError. All 30 sites now go through them. rescue/fsbounds.py's own follow_symlinks arguments are deliberately untouched: those are os.DirEntry objects from os.scandir, where the keyword has been valid all along. user_profile_size additionally needed a testability seam. Its tests mocked only subprocess and then walked the real /Users, so they asserted against whatever happened to be on the host — passing on a developer's Mac and failing anywhere else, which is how the follow_symlinks breakage stayed hidden. The traversal roots are now attributes a test can point at a fixture tree, and the tests build one, covering the skip rules, a missing /Users, and a failing du. They no longer touch the host filesystem. Also drops CliRunner(mix_stderr=False) in test_cli_scan_json: click >= 8.2 removed the parameter and now always captures stdout and stderr separately, which is precisely what these tests want to assert. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013g8dGcNgzJ1vZCw6kguGEu
The remaining failures were not flaky. Each was a test that silently depended on the machine it ran on, so the suite only passed on one developer's macOS box: - notification_center_check (7), kext_audit (1), appleid_security_check (1): each module checks a real path under ~/Library or /Library and returns early if it is absent, so on any non-macOS host check() short-circuited before the mocked subprocess was ever reached. The traversal roots are now attributes a test can point at a fixture; appleid additionally needed a real plist on disk, since patching plistlib.load still left open() to fail. - win_safe_mode_check (2): a time bomb. The uptime fixtures were absolute dates chosen to be "5 days ago" when written; real time moved past them and the 5-day fixture aged into a >30-day uptime, tripping the high-uptime warning it asserts is absent. Fixtures are now computed relative to now. - disk_permissions_repair (1): asserted ownership against a hardcoded uid 501, so it only passed as a typical macOS user and failed as root. It now derives the uid from the process, with /usr/local given a separate non-root uid — "correctly owned" means the running user for a home directory but explicitly *not* root for /usr/local, which one uid cannot express when running as root. - update/test_verify (1): the test signs a tag with `git tag -s` without pinning gpg.format. git honours the ambient setting, so on a machine configured for SSH commit signing it produced an SSH signature while the test asserted on the GPG path. Pinned to openpgp. The verification code itself is correct here — git verify-tag detects the signature type on its own — so only the test changed; no trust check was weakened to get this green. kext_audit's `find` call also gained the timeout it was missing. Full suite: 3405 passed, 0 failed (was 64 failed, 3341 passed). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013g8dGcNgzJ1vZCw6kguGEu
docs/ROADMAP.md P0#8 recorded that digital_security_reset referenced password_manager_check, twofa_audit, and session_revocation_scan while none was registered. The recorded fix deleted the references and relabelled the steps human-led, so the capability was never actually built. These are the real modules. All three are read-only and guidance-only (ActionKind.GUIDANCE, no auto_apply), and none of them reads a secret: no vault, keychain item, cookie database, token value, or SSH key material. Their filesystem roots are class attributes so the tests run off a fixture tree rather than the host. password_manager_check — detects installed managers (macOS app bundles, Windows uninstall registry) and browser profiles that would otherwise be holding the passwords. Browser storage is flagged only when no dedicated manager is present, since that is the posture worth changing. Prefix matching stops at the first hit so KeePassXC is not also reported as KeePass. twofa_audit — the honest one. A local tool cannot tell whether 2FA is enabled on your accounts; that lives at the provider and confirming it means signing in. So it reports what it can observe (authenticator apps, locally recorded platform 2FA) and states account status as NOT CHECKED. Crucially, absence of evidence is reported as unknown, never as "two-factor is off" — an unverified security verdict presented as fact is worse than an admitted gap, and there is a test asserting it can never collapse to False. session_revocation_scan — the post-compromise question: what is still logged in. Counts browser profiles (each holds its own cookies), device-configured accounts, and authorised SSH keys, all of which survive a password change. The guidance puts the password change *before* the sign-out, because revoking first just lets whoever has the password back in, and that is the step people get backwards. A test asserts key material never reaches a finding. Registry now discovers 275 modules; catalog and remediation-code gates pass. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013g8dGcNgzJ1vZCw6kguGEu
docs/ROADMAP.md P2 lists rescue techniques "missing as coherent, evidence-driven rescue techniques rather than another collection of isolated checks". Three of those rows were module-shaped and had no coverage at all. code_signature_audit — the answer to P2's own complaint that "file-name and keyword checks create false positives". Verifies signatures via codesign/spctl and Get-AuthenticodeSignature. Severity is chosen to stay defensible: CRITICAL only for a signature that is present and broken, because a binary modified after signing has no innocent reading; WARNING for unsigned software in a system-wide location; nothing at all for unsigned software in the user's own ~/Applications, which would be pure noise. Verification is slow, so the scan is capped — and the INFO finding states the cap and whether it was hit, because a result that silently examined 40 of 300 apps while implying a full audit is worse than none. security_baseline_diff — P2's "a single snapshot cannot identify what changed". Records persistence, listening ports, browser extensions and protection states, then reports only additions and only protections that went on -> off. The asymmetry is deliberate: reporting removals and re-enablements symmetrically buries the four changes that matter under forty that do not. Two properties are tested rather than assumed — every finding states the trust-on-first-use limit (a machine already compromised at baseline has that baked in), and a protection that merely became unqueryable is never reported as disabled. evidence_bundle — P2's "repair can destroy the information needed to understand a compromise". Runs at priority 90 so it is considered before the modules that propose fixes. check() deliberately writes nothing; creating files is a real effect and belongs behind an explicit human decision. The bundle it can write is redacted by construction, hashes every item, and records what it omitted and why so a gap is visible to the recipient rather than looking like an absence of evidence. It has no network path. A test asserts no collection command can read a credential store. digital_security_reset now includes these plus the three account-recovery modules, with evidence_bundle first. Its description no longer claims the password-manager, 2FA and session steps are entirely human-led, because the device-side half of each is now real — while being explicit that changing passwords, enabling 2FA and revoking sessions still happens at the provider. Also regenerates rescue/security/integrity_manifest.json, which was stale on main: verification reported 8 tampered and 5 added files, so every launch printed a tamper warning. It verifies clean now. Registry discovers 278 modules. Full suite: 3503 passed, 0 failed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013g8dGcNgzJ1vZCw6kguGEu
Three claims in the status document did not hold on main, and a status doc that says "verified" next to something untrue is worse than no status doc: - P0#4 self-integrity was recorded as verifying clean. On main it reported 8 tampered and 5 added files, so every launch printed a tamper warning. The regeneration was real but happened on a different lineage. - P0#8 was recorded as done. The profile was made valid by deleting the three module references, not by building the modules. - The baseline was recorded as 3094 passing. It was 64 failed / 3341 passed, with one file that did not parse on the declared minimum Python. The corrections are appended as a dated section rather than edited in place, so what was previously believed stays legible. Also records the current numbers for the two large outstanding migrations, both of which have grown since the roadmap was written (756 subprocess.run sites with 0 through the bounded runner, 435 without timeouts; 169 modules with no emits_codes), and the absence of CI as the gap that let the Python 3.11 breakage ship. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013g8dGcNgzJ1vZCw6kguGEu
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.
Consolidates the three diverged branches onto
main, greens a suite that had 64 failures, and implements the six planned modules the roadmap named but never built.Full suite: 3503 passed, 0 failed (from 64 failed / 3341 passed). Registry discovers 278 modules.
rescue/security/integrity_manifest.jsonverifies clean.Branch consolidation
maincould not merge any of the three branches: they have completely unrelated histories, no common ancestor at all.main's own root commit is a mid-stream feature commit, so it looks like a re-rooted snapshot rather than a continuation.Rather than force a ~400-file conflict through
--allow-unrelated-histories, I compared the trees:mainwip-checkpoint-2026-07-16feat/core-frameworkclaude/bloatware-spyware-remediation-0mbzkbOnly the third carried unique content, so those 3 commits are cherry-picked here (cryptojacking IOC pack,
stalkerware_scan, LAN inventory / ARP-spoof / router-security modules, and thehome_network_intrusionandidentity_theft_recoveryprofiles and guides).git diffagainst every branch tip now reports zero missing files, and the merge introduced zero regressions.This makes PR #3 redundant — its content is fully contained here, and its base branch is one of the branches being retired.
Audit findings
Several of these contradicted
docs/ROADMAP_STATUS.md, which is corrected in a dated section rather than edited in place, so what was previously believed stays legible.Python 3.11 support was broken, not partially broken.
pyproject.tomldeclares>=3.11, but:follow_symlinks=topathlib.Path.is_file()/is_dir(). That keyword arrived in 3.13, so every one raisedTypeErrorat runtime. Those modules did not work on two of the three supported versions.rescue.fsboundsnow exposesis_file_nofollow/is_dir_nofollow, which get the same no-follow semantics fromos.lstateverywhere.fsbounds' ownfollow_symlinksarguments are deliberately untouched — those areos.DirEntryfromos.scandir, where the keyword has always been valid.The integrity manifest was invalid on
main— 8 tampered, 5 added files, so startup verification failed and every launch printed a tamper warning.ROADMAP_STATUS.mdrecorded this as verifying clean; the regeneration was real but happened on the other lineage, andmain's commits then addedremediation.py,serialize.py,threat_map.pyand two TUI screens without regenerating. Fixed.There is no CI. No
.github/directory exists. A matrix that actually ran the suite on 3.11 would have caught the largest defect in the tree.Verified healthy, for balance:
auto_apply = Trueappears in zero modules, so auto mode genuinely is read-only as P0#5 intends; noNotImplementedErrorstubs or placeholder TODOs; the only skipped tests are correctly conditional ongpg; 272 pre-existing modules discover cleanly with no duplicate names; and bothsetup.pyandrescue.specglob the content directories, so new modules ship without packaging changes.The 64 failures were environment coupling, not flakiness
Every one was a test that silently depended on the machine it ran on, so the suite only passed on one developer's macOS box:
notification_center_check,kext_audit,appleid_security_checkeach check a real path under~/Libraryor/Libraryand return early if absent, socheck()short-circuited before the mocked subprocess was reached.appleidadditionally needed a real plist on disk, since patchingplistlib.loadstill leftopen()to fail.win_safe_mode_checkused absolute dates chosen to be "5 days ago" when written; real time moved past them and the fixture aged into a >30-day uptime, tripping the warning it asserts is absent. Fixtures are now relative.disk_permissions_repairasserted ownership against a hardcoded uid 501, so it only passed as a typical macOS user. It now derives the uid from the process, with/usr/localgiven a separate non-root uid: "correctly owned" means the running user for a home directory but explicitly not root for/usr/local, which one uid cannot express when running as root.CliRunner(mix_stderr=...), removed in click 8.2.update/test_verifysigned a tag without pinninggpg.format, so on a host configured for SSH commit signing it produced an SSH signature while asserting on the GPG path. No trust check was weakened to green this:git verify-tagdetects signature type on its own, the verification code was correct, and only the test changed.follow_symlinksbreakage above.The fix pattern is now a convention: a module's traversal roots are class attributes so tests can point them at a fixture tree.
Desktop IPC hardening
ipcMain.handle('open-setting', …)passed any renderer-supplied string straight toshell.openExternal, which hands the URL to the OS handler.The renderer escapes all scan output, so there is no live injection path today. But the URLs it opens come from
permissions-content.json— precisely the kind of data file the signed-content-update mechanism exists to replace. Left unrestricted, the channel turns "attacker influences shipped content" into "attacker launches local files and registered protocol handlers", a bad trade for a tool people run on machines they already suspect are compromised.The walkthrough only opens OS settings panes, so this is an allowlist of scheme prefixes rather than sanitisation — allowlists have no parser differentials, and matching is
startsWithso an allowed scheme embedded later in afile://URL does not qualify it. Also setssandbox: true, denieswindow.open, and blocks navigation. The predicate lives indesktop/setting-urls.jsso it is testable without booting Electron, andpackage.jsongains thetestscript the desktop app was missing.Planned modules
Resolved against the roadmap, not guessed. Profiles, guides and the threat map reference no missing modules, and every module named in
docs/superpowers/plans/exists. Two sources yielded real gaps:password_manager_checktwofa_auditsession_revocation_scancode_signature_auditsecurity_baseline_diffevidence_bundleP0#8's three were recorded as "done" because the profile was made valid by deleting the references — the capability was never built.
All six are read-only, guidance-only (
ActionKind.GUIDANCE, noauto_apply), read no secret of any kind, and bound their filesystem and command work. Three design points worth review attention:twofa_auditrefuses to overclaim. A local tool cannot know whether 2FA is on at your provider; confirming it means signing in. So it reports what it observes and marks account status NOT CHECKED, and absence of evidence is reported as unknown, never as "two-factor is off". There is a test asserting it can never collapse toFalse.security_baseline_diffis deliberately asymmetric. New persistence/port/extension warns; removals do not. Protection going on → off is CRITICAL; off → on is silent. Symmetric reporting buries the four changes that matter under forty that do not. Every finding states the trust-on-first-use limit, and a protection that merely became unqueryable is never reported as disabled — both tested.evidence_bundle.check()writes nothing. Creating files is a real effect and belongs behind an explicit human decision. Its bundle is redacted by construction, hashes every item, records what it omitted and why, and has no network path. A test asserts no collection command can read a credential store.digital_security_resetnow includes these withevidence_bundlefirst, and its description no longer claims the password-manager/2FA/session steps are entirely human-led — the device-side half is real now, while the provider-side work stays human-led.Test plan
grep -rnE '\.(is_file|is_dir)\(follow_symlinks' modules/returns nothing.discover_modules;tests/test_all_shipped_content.py,tests/test_remediation_validation.pyandtests/test_module_code_consistency.pyall pass. That last one caught a real defect during development — codes built with an f-string instead ofcode="..."literals — which is fixed.python3 scripts/generate_integrity_manifest.pythenverify_package_integrity→ clean.node --testindesktop/→ 7 passing.Follow-ups not in this PR
wip-checkpoint-2026-07-16,feat/core-frameworkandclaude/bloatware-spyware-remediation-0mbzkbafter this merges, and close the now-redundant PR Cryptojacking, home-network intrusion, stalkerware, and identity theft recovery #3.subprocess.runcalls in modules, 0 throughrescue.command.run, 435 without timeouts. The roadmap counted 744/395 — both have grown.emits_codesmigration: 169 of 272 pre-existing modules declare none.Both migrations are large mechanical sweeps that deserve their own PRs.