Skip to content

0.3.5: persistence AV engine, host persistence targets, Security removal, 153 recipes - #15

Merged
ibnbd merged 9 commits into
mainfrom
arena/f8ff7b3f-cleanton
Oct 25, 2026
Merged

ibnbd merged 9 commits into
mainfrom
arena/f8ff7b3f-cleanton

Conversation

@ibnbd

@ibnbd ibnbd commented Oct 25, 2026

Copy link
Copy Markdown
Contributor

Summary

Ninth pass at the 0.3.5 list. The headline is a new antivirus detection engine, wired so it actually inspects the machine, plus Security-tab removal that now exists in both UIs with one shared policy.

Nothing here has been compiled (no Rust toolchain in this sandbox) - this PR is the CI gateway: cargo fmt --check, cargo clippy --workspace --all-targets --all-features -- -D warnings, cargo test --workspace --all-features, cargo audit.

Antivirus: new persistence engine

Every other engine asks is this code malicious? This one asks has something already made itself at home? It inspects the four families of files whose only job is to run something later (launchd plists, shell startup files, cron drops, systemd user units) and fires only on the payload, never on existence:

  • PERSIST_LAUNCHD_DOWNLOAD_EXEC / _DECODE_EXEC - agent fetches or decodes and runs code
  • PERSIST_LAUNCHD_TEMP_EXEC[_RUNATLOAD] - executable in /tmp or a hidden dir
  • PERSIST_LAUNCHD_INLINE_SCRIPT - -c/-e body that downloads, decodes, or hides
  • PERSIST_SHELLRC_* - curl|sh, base64-eval, launchctl load, detached nohup
  • PERSIST_CRON_* / PERSIST_SYSTEMD_* - @reboot and short-interval beacons

24 unit tests, including the true negatives that matter: a normal LaunchAgent, an ordinary .zshrc, a commented-out payload, a normal crontab, and a binary plist all stay clean. Dot-directories that are the ordinary home for user tooling (~/.local, ~/.cargo, ~/.cache) are explicitly not treated as hidden, which is where a naive version of this check produces false positives.

Crucially it is wired to real host state: new SecurityTargetKind::HostPersistence scans ~/Library/LaunchAgents, ~/.config/systemd/user, and the nine shell-rc names in $HOME. Without this the engine would only ever fire on packages that ship persistence files.

Security removal, in both UIs from one policy

SecurityRemoval now lives in cleanton-engine as the single source of truth for what is removable and how, so the Slint app and the TUI cannot drift:

  • Slint: two-step arm, then confirm/cancel on each row; npm/pip through PackageManagers::uninstall, everything else through the executor's reviewed trash_suggestion. ActionButton gained a danger tint so a destructive control never reads as an ordinary secondary button.
  • TUI: [r] arms, [y] runs, [n] cancels; moving the cursor disarms. Same two paths, same engine copy.

Host persistence files get no Remove button and an "open it and delete the flagged line" remedy. Trashing a user's .zshrc to fix one line would be the wrong fix, so Cleanton refuses it in both UIs, with a defence-in-depth guard in the removal runner too.

Background helper: disk/memory cleanup

resource_pressure_events was emitting whitespace-mangled strings ("...and clean <many spaces> caches") from broken string continuations. Replaced with a real measurement: a bounded (20k-entry budget) walk names the top-3 reclaimable directories plus the Trash.

Disk is 93% full (48 GB available). Biggest reclaimable right now: Xcode (18.4 GB), Trash (6.1 GB), ms-playwright (2.2 GB).

Edge-triggered as before, and the budget means it can never stall the watcher.

Recipes: 141 to 153

  • 22 macOS-only recipes gained Linux ~/.cache/* alternatives (pip, poetry, Go, Deno, Cypress, Playwright, Yarn, Coursier, JetBrains, Firefox, Slack, Zoom, Spotify, Android Studio...). This is the biggest single fix for "the scanner misses things on Ubuntu".
  • 12 new: Edge/Brave/Vivaldi HTTP caches, Discord, node bundler caches, thumbnails, Trivy/Grype/Snyk caches, apt/snap/flatpak caches, Helm, Firebase emulators, Nim, Dune. All documented in doc/RECIPES.md.

Slint UI fixes

  • Caught before shipping: cross-axis-self-alignment only exists in Slint 1.18; this tree pins 1.17.1. Centring now uses cross-axis-alignment on the container or a wrapper HorizontalLayout { alignment: center; }.
  • Transient spinners and the activity bar are conditional if, not visible: false (which still occupies layout and shifts rows).
  • Duplicate sets render as a set header plus one row per copy, capped at 12 copies / 25 sets.

Also

  • Version 0.3.5.
  • scripts/check-ui-copy.py (em-dash guard for user-visible copy) is wired into CI as a copy job ahead of audit; it passes.

Known gaps

  • Scanner performance is untouched this round. The prior generation's hot paths are already fixed; the remaining wins are allocation/traversal in walker.rs / metadata.rs, and guessing at them without a profiler seemed worse than leaving them.
  • doc/*.md prose still carries em dashes (the guard covers shipped UI copy, not developer docs).
  • cleanton-antivirus is vendored from rb-antivirus. The new engine is a self-contained engines/persistence.rs touching only three lines of upstream engines/mod.rs, so it should sync cleanly in either direction.

ibnbd and others added 9 commits October 25, 2026 17:05
…oval

Bugs 2-6 from the 0.3.5 list, plus scanner coverage and the background
resource helper.

Antivirus
- New `persistence` engine (10th): inspects launchd plists, shell startup
  files, cron drops, and systemd user units for payloads that run again
  later. Fires only on the payload (temp/hidden executables, download-exec,
  decode-exec, launchctl load, @reboot beacons), never on existence.
- New `SecurityTargetKind::HostPersistence` so the engine sees real host
  state: ~/Library/LaunchAgents, ~/.config/systemd/user, and the shell-rc
  names in $HOME. Those rows are deliberately not removable, and get an
  "edit the line, not the file" remedy.

Security removal (both UIs, same policy)
- `SecurityRemoval` in cleanton-engine is now the single source of truth for
  which flagged packages are removable and how. The Slint app and the TUI
  both drive it, so they cannot disagree.
- Slint: two-step arm/confirm/cancel, npm/pip via PackageManagers, else the
  executor's reviewed Trash path. ActionButton gains a `danger` tint.
- TUI: [r] arms, [y] runs, [n] cancels; moving the cursor disarms.

Background helper
- Fix the whitespace-mangled disk/memory suggestions. A pressure event now
  names the top-3 reclaimable directories plus the Trash, measured against a
  20k-entry budget so the walk can never stall the watcher.

Recipes (141 -> 153)
- 22 macOS-only recipes gain Linux ~/.cache alternatives.
- 12 new: Edge/Brave/Vivaldi HTTP caches, Discord, node bundler caches,
  thumbnails, Trivy/Grype/Snyk, apt/snap/flatpak, Helm, Firebase emulators,
  Nim, Dune. All documented in doc/RECIPES.md.

Slint fixes
- Centring uses cross-axis-alignment (1.17) or a wrapper layout;
  `cross-axis-self-alignment` does not exist until Slint 1.18 and the tree
  pins 1.17.1.
- Transient spinners/activity bar are conditional `if`, not `visible:false`
  (which still occupies layout).
- Duplicate sets render per-copy rows with a set header, capped.

Version 0.3.5. `scripts/check-ui-copy.py` guards user-visible copy and is
wired into CI.

Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
rustfmt (run as a parser check) found two things CI would have rejected:

- persistence.rs: the download/decode-exec regexes used `r"..."` delimiters
  but their character classes contain a bare `"`, which terminated the raw
  string early. They are `r#"..."#` now.
- security.rs: `PERSISTENCE_REMEDY` put the line-continuation backslash after
  `=` instead of inside the string literal.

Then `cargo fmt` over every touched file.

Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
- background.rs: drop the redundant `.into_iter()`; replace
  `push_str(&format!(..))` with plain appends.
- security.rs: fold the two identical `return None` arms into one; move the
  host-persistence roots into `push_host_persistence_targets` so
  `discover_default_targets` stays under the line limit; compare file
  extensions with `Path::extension()` rather than a case-sensitive
  `ends_with`.

Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
`builtin_recipe_self_tests_are_valid` pins the number of registered recipes so
an unregistered recipe file (or a dangling registration) fails loudly. Adding
12 recipes invalidated it; the count is bumped by hand, as intended.

Also removes the temporary workflow that mirrored clippy/test output into
check annotations: the gates are readable now.

Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
@ibnbd
ibnbd merged commit 558acfa into main Oct 25, 2026
10 checks passed
@ibnbd
ibnbd deleted the arena/f8ff7b3f-cleanton branch October 25, 2026 19:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant