No description
Find a file
Hound 909afacb19 houndd: rewrite the rootkit checks so they stop crying wolf
Phase 5's correctness half. The previous implementation could not ship:
its two main checks were structurally wrong rather than badly tuned.

  hidden processes  was "any /proc/<pid> whose comm we cannot read",
                    which fires on every process that exits between the
                    listing and the read. A race, not a signal.
  setuid anomalies  compared against a hardcoded allowlist of binary
                    names, written on one distribution.

Replaced with questions that have factual answers:

  A process is hidden when the kernel agrees it exists and /proc does
  not list it. kill(pid, 0) answers the first half for the whole PID
  space — ESRCH means gone, EPERM means it exists and belongs to
  somebody else, which is the case that matters since a rootkit's
  process will not be ours. The sweep is bracketed by two listings and
  candidates are re-verified, so a process that merely started or
  exited during the scan cannot be mistaken for a hidden one.

  A setuid binary is suspicious when no installed package claims it.
  The package manager already knows what belongs on the system.

Two bugs found by testing against this machine rather than reasoning
about it, both of which would have made the feature useless in the
field:

* /proc lists thread-group leaders; kill() accepts any THREAD id. A
  process with twenty threads therefore has nineteen ids that answer
  kill and appear in no /proc listing. Comparing against the pid set
  alone reported dozens of criticals on a completely healthy laptop.
  The honest set is the union of leaders and their /proc/<tgid>/task
  entries.

* Merged-/usr breaks package ownership in BOTH directions. /bin is a
  symlink to usr/bin, so every binary has two names, and dpkg's own
  index is inconsistent about which it records: sudo.list says
  /usr/bin/sudo while fuse3.list says /bin/fusermount3 and cifs-utils
  says /sbin/mount.cifs. String comparison reported the entire setuid
  set as unowned. Both spellings now go into the index, candidates are
  deduplicated by resolved path, and lookups try both.

Also: ld.so.preload is now checked (it is empty on a healthy system and
is the classic userland rootkit), the writable-directory check no
longer counts sticky-bit directories, and the hidden-file check uses
symlink_metadata so an ordinary dangling symlink is not an incident.

Verified on this machine, privileged and not: clean, 0 findings, 2.8s
including the full 4.2-million-pid sweep. The exit criterion asks for
five machines across three distributions and only one was available
here, so treat the cross-distro half as unmet.

The regression tests are the point: a scan run while processes churn
continuously must produce no criticals, and a process with eight live
threads must not produce eight findings.

200 tests pass across the workspace.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 06:34:26 -05:00
assets/icons packaging: .deb, AppImage, rpm spec, PKGBUILD, hardened unit, app icon 2026-08-20 23:37:17 -05:00
crates houndd: rewrite the rootkit checks so they stop crying wolf 2026-08-21 06:34:26 -05:00
dist packaging: .deb, AppImage, rpm spec, PKGBUILD, hardened unit, app icon 2026-08-20 23:37:17 -05:00
gui packaging: .deb, AppImage, rpm spec, PKGBUILD, hardened unit, app icon 2026-08-20 23:37:17 -05:00
packaging packaging: .deb, AppImage, rpm spec, PKGBUILD, hardened unit, app icon 2026-08-20 23:37:17 -05:00
.editorconfig Rust engine + CLI over ClamAV Unix socket 2026-08-20 16:59:14 -05:00
.env.example Rust engine + CLI over ClamAV Unix socket 2026-08-20 16:59:14 -05:00
.gitignore Full feature set: realtime monitor, quarantine vault, rootkit scan, settings, events 2026-08-20 20:33:44 -05:00
Cargo.lock hound-supply: the supply-chain and agent-era scanner 2026-08-20 23:48:48 -05:00
Cargo.toml hound-supply: the supply-chain and agent-era scanner 2026-08-20 23:48:48 -05:00
README.md Engine seam (ScanEngine trait) + Tauri GUI 2026-08-20 17:55:40 -05:00
rust-toolchain.toml Rust engine + CLI over ClamAV Unix socket 2026-08-20 16:59:14 -05:00

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 main is 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.