The old engine shelled out to clamscan for every scan, and clamscan
reloads a 169 MB signature database on every invocation. Measured on a
68-byte EICAR file: 6.5 seconds and ~1.5 GB RSS — paid once per file,
and realtime.rs called it once per inotify event.
Replaces it with HoundEngine: yara-x compiled once at daemon start,
held in memory, one scanner reused across a whole walk, plus a verdict
cache keyed on (dev, ino, mtime, size) so an unchanged file that has
been seen before never reaches the matcher.
Measured after, same machine, same EICAR file:
single file 6.5 s -> 4 ms
400 files cold -- -> 9 ms
400 files warm -- -> 5 ms
Also here:
- rules.rs: hot-swappable rule store. Built-in pack is embedded so a
fresh install detects something before it has ever reached the
network; on-disk packs load from $HOUNDD_RULES_DIR, /var/lib/hound
or the XDG data dir. Reload swaps an Arc, so in-flight scans are
never torn out from under.
- cache.rs: bounded FIFO verdict cache. Any of the four key fields
changing means rescan, so edits, truncates and replace-by-rename all
correctly miss.
- The goodware gate: every rule is scanned against all of /usr/bin,
/bin and /usr/sbin in CI, and a single hit fails the build. It has
already earned its keep — it caught a reverse-shell rule that matched
/usr/bin/sudo, which is now removed rather than tuned. A rule that
quarantines sudo is worse than no rule at all.
- ScanEngine is Send + Sync and selection stays per-call, so
HOUNDD_ENGINE=clamav still reaches the legacy path for comparison.
- ScanResult.skipped reports files passed over for size instead of
quietly counting them as clean.
- Settings gain theme (auto/light/dark), tray_icon_style (color/mono),
close_to_tray and confirm_quit, normalised daemon-side because
clients are not trusted to send a theme we can render.
57 tests pass, up from 29.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
ClamAV is a temporary dependency. Everything ClamAV-specific moves
behind a 4-method trait in crates/houndd/src/engine.rs:
trait ScanEngine { name; probe; scan; update }
struct ClamAvEngine // today
const ENGINE // one-line flip when the native engine lands
- parse_clamscan() is now a pure fn with unit tests (OK/FOUND/INCOMPLETE,
dedup, malformed lines)
- main.rs drops all clamscan/clamd/freshclam knowledge; Status reports
engine='clamav' via the trait
- README documents the seam and the wire contract that stays stable
GUI (gui/, Tauri 2, standalone workspace, vanilla JS premium-dark shell):
- system-tray sentinel on the 4-state dog-head ladder (green/amber/red/gray)
with tooltips; clicks open the window / scan / update
- window: protection hero, path + recursive scan, progress bar, results
table, signature update log; state mirrored to the tray
- compiles clean (cargo build, 0 warnings) on Mint 22.3 + webkit2gtk-4.1
All 11 workspace tests pass, incl. the E2E EICAR scan over the real
Unix-socket daemon (clamscan finds the planted EICAR).
- hound-api: DbFile (file + updated_at), Status.db, UpdateResult, Client::update()
- houndd: probe() extracts signature freshness from /var/lib/clamav mtimes;
update() tries 'sudo freshclam', falls back to plain 'freshclam' and
reports the honest reason on failure; output capped to 2KB tail
- hound CLI: 'hound update' with --json parity and human output
- rename Status.clamav_present -> engine_present + add Status.engine
(prep for the engine seam; wire stays engine-agnostic)
Live-verified: status shows DB file + last-modified RFC3339; update
returns ok:false with the real log for a plain user (exit 1).