A clean laptop reported 988 critical rootkit findings; this server
3786, PID 1 among them. Every one was false, and the cause was our own
systemd hardening.
ProtectProc=invisible hides processes the daemon does not own from its
view of /proc, while kill(pid, 0) keeps answering truthfully because it
is a syscall and not a filesystem lookup. The hidden-process check
compares exactly those two sources, so with that setting every process
on the machine looked concealed. Enumerating processes is this daemon's
job, so it needs the default view.
Removing the setting is not enough on its own — hidepid= on the /proc
mount produces the same blindness and we do not control that. So the
detector now recognises when it cannot see:
- PID 1 is the control. It always exists and nothing hides init; a
rootkit that did would break the machine it is living on. If PID 1
answers kill(1, 0) but is absent from the listing, we are blind and
say so as info rather than crying rootkit.
- A plausibility ceiling of 32. Hiding a handful of processes is the
entire point of a rootkit; hundreds means a broken observer. An
antivirus that reports a critical rootkit finding on every clean
machine teaches people to ignore the one time it is real.
Also in this change, from testing on a real desktop:
- Closing the window hides it to the tray instead of exiting, with a
one-time notification so it does not read as a crash. Quit lives
only in the tray menu and confirms first. The settings already had
close_to_tray and confirm_quit fields wired to nothing; they are
honoured now rather than hardcoded.
- The tray menu and Scan Home sent the literal string "~". A shell
would have expanded it, nothing here did, so the daemon was asked
to scan a directory of that name. It failed silently until the
per-peer readability check made it audible.
- Administrative actions elevate through polkit instead of telling
people to open a terminal. The app tries unprivileged first and
only on a privilege refusal runs `pkexec hound admin-rpc`, which
forwards one request as root. auth_admin_keep, because prompting on
every settings toggle trains people to authenticate without reading
the prompt. This grants what `sudo hound` already grants to people
who could already run sudo — a transport, not a new privilege.
- `hound settings exec-gate on|off` now exists. The install script,
the AppImage banner, the rpm spec, the AUR install file and
llms.txt all told users to run `hound settings set exec_gate true`.
There was no `set` subcommand and no way to enable the execution
gate from the CLI at all: the flagship paid feature was unreachable
and the first thing a new user was told to type returned an error.
A test now asserts every documented command parses.
- `settings show` displays the exec gate state, and no longer prints
its own header twice.
- The CLI help still described ClamAV, which has not been the engine
for some time. So did the socket permission error, which now
explains the `hound` group and the log-out-and-back-in it needs.
368 tests pass.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|---|---|---|
| assets/icons | ||
| crates | ||
| dist | ||
| gui | ||
| packaging | ||
| site | ||
| tools | ||
| .editorconfig | ||
| .env.example | ||
| .gitignore | ||
| Cargo.lock | ||
| Cargo.toml | ||
| README.md | ||
| rust-toolchain.toml | ||
Hound Antivirus
A premium, freemium antivirus for Linux. One engine, three faces:
a Rust daemon (houndd), a CLI (hound), and a Tauri GUI with a
system-tray sentinel that changes color with your security state.
Built for the distros people actually run: Ubuntu, Debian, Linux Mint, and anything else that ships ClamAV.
Repository layout
antivirus/
├── Cargo.toml # Rust workspace
├── crates/
│ ├── hound-api/ # shared wire types + socket client (daemon/CLI/GUI all use it)
│ ├── houndd/ # the daemon: Unix-socket API over a pluggable engine
│ └── hound/ # CLI client
├── assets/icons/ # dog-head brand mark + 4-state tray ladder
└── gui/ # Tauri 2 desktop app (system tray + scan UI)
Architecture
houndd (Rust daemon — the engine)
┌──────────────────────────────────┐
│ ScanEngine trait │
│ ├─ L1 ClamAV signatures (now) │
│ ├─ L2 Curated threat packs(Pro)│
│ ├─ L3 Behavioral monitor (Pro) │
│ └─ L4 Supply-chain checks(Pro) │
└──────────────┬───────────────────┘
Unix socket (JSON-RPC, line-delimited)
┌───────────┼───────────┐
hound CLI GUI (Tauri) future modules
The daemon is the only process that touches a scanning engine. CLI and GUI are thin clients — so future suite tools (firewall, updater, …) plug into the same socket.
Swapping the engine (the ClamAV seam)
ClamAV is a temporary dependency. Everything ClamAV-specific — version
probe, signature freshness, the clamscan subprocess + output parsing,
freshclam — lives in one file behind a four-method trait:
crates/houndd/src/engine.rs
trait ScanEngine { name; probe; scan; update }
struct ClamAvEngine // today
const ENGINE: ClamAvEngine // ← flip this line when the native
// engine lands; nothing else in the
// daemon, CLI, GUI, or wire API moves
The wire stays engine-agnostic: Status.engine names the implementation
("clamav" today) and Status.db carries what any signature store has —
a file name and a timestamp. When our own Rust engine ships, it's a new
ScanEngine implementation, a one-const flip, and the tray/CLI/GUI simply
start reporting the new engine name.
Icon system
The brand mark is a solid dog head (assets/icons/hound.svg), a single
flat fill. It ships in two treatments:
- Brand ladder
hound-{16,22,24,32,48,256}.png— native periwinkle#9896E0, for the window icon, About box, and marketing. - Tray-state ladder
state-<name>-{16,22,24,32,48}.png— the same path re-tinted per security state, for the system-tray sentinel:
| State | Fill | Meaning |
|---|---|---|
protected |
#22C55E green |
up-to-date / protected / clean |
scanning |
#F59E0B amber |
scan in progress / signatures need update |
threat |
#EF4444 red |
infection found |
paused |
#6B7280 gray |
real-time monitor off |
Green is the good state; amber is work in progress, never a failure.
Quickstart (development)
Prereqs: Rust (see rust-toolchain.toml), Node 20+, ClamAV, and the
Tauri system libs (libwebkit2gtk-4.1-dev, libgtk-3-dev, libayatana-appindicator3-dev).
# 1. Signatures (needs the clamav freshclam DB)
sudo freshclam
# 2. Daemon (terminal 1)
cargo run -p houndd
# 3. Scan (terminal 2)
cargo run -p hound -- status
cargo run -p hound -- scan ~/Downloads
# 4. GUI
cd gui && npm install && npm run tauri dev
Verifying the engine with the EICAR test file
EICAR is the industry-standard 68-byte test signature — every AV that works will flag it. Generate it and scan it:
printf 'X5O!P%%@AP[4\\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*' > /tmp/eicar.com
cargo run -p hound -- scan /tmp/eicar.com
# expect: exit code 1, "Eicar-Test-Signature FOUND"
Updating signatures
hound update wraps freshclam (trying sudo freshclam first, since
plain-user runs can't write /var/lib/clamav and /var/log/clamav). The
GUI's "Update Signatures" button drives the same RPC and shows the log.
cargo run -p hound -- update # or: hound update --json
The GUI (gui/)
A Tauri 2 desktop app — a thin view over the same houndd socket the
CLI uses (via the shared hound-api client), so the window and the
command line never disagree about your security state.
gui/
├── dist/ # the front-end (vanilla HTML/CSS/JS, premium dark shell)
└── src-tauri/ # Tauri 2 shell + system-tray sentinel
The tray sentinel swaps the 4-state icons (green/amber/red/gray) as your state changes; the window shows a live protection hero, a scan progress bar, a results table, and the signature-update log.
Build it:
cd gui
npm install
npm run tauri dev # dev with hot reload
npm run tauri build # → .deb in src-tauri/target/release/bundle/
Git conventions
-
Branch
mainis deployable; small, focused commits. -
No hardcoded secrets. For pushes, the bot token lives in a repo-local credential file (never tracked):
git config credential.helper 'store --file=.git/.git-credentials' chmod 600 .git/.git-credentials echo 'https://<user>:<token>@git.joelovestech.com' >> .git/.git-credentials -
Commits: imperative subject, ≤ 72 chars. Example:
houndd: add line-delimited JSON-RPC socket API
License
TBD — core daemon likely proprietary (freemium), shared CLI possibly OSS.
Decision pending; workspace.package.license = MIT is a placeholder.