Commit graph

4 commits

Author SHA1 Message Date
dev
cb943c66c4 0.1.2: hound update brings the whole machine current
Definitions and the application were two separate acts with two
different mechanisms, and only one of them had a command. `hound
update` now does both:

  sudo hound update                definitions, then offer the app
  sudo hound update -y             both, unattended
  hound update --check             what is available, install nothing
  sudo hound update --definitions-only

Definitions run first and independently: a release check that fails —
laptop offline, host down — is a warning, not a failed command. The
definitions half is what matters between releases and it either worked
or it did not, regardless of what the manifest server did.

The app half prompts unless given --yes, because definitions are
verified before they are parsed while replacing the running binary is a
larger step. Non-interactive callers never block on a read: they are
told to pass --yes and exit cleanly.

`release.check` is classified read-only, so any member of the `hound`
group can ask whether they are current — it changes nothing and reveals
nothing the website does not. Installing still requires root and says
so.

Verified end to end on a real machine: a 0.1.1 install discovered
0.1.2, verified the signed manifest, checked the SHA-256, staged it
root-owned and installed it through apt, unattended.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 12:10:20 -05:00
dev
38d1feeb0c 0.1.1: signed release notifications, and definitions that actually update
Two mechanisms Joe asked for, neither of which existed.

**Definitions now update themselves.** `auto_update_signatures` shipped
from the start and nothing ever read it — definitions only moved when
somebody typed `hound update`, which for a security product means most
installations were running whatever they were installed with. There is
now a scheduler thread: definitions hourly, release check daily, and a
90-second startup delay so a fleet that reboots together does not
arrive at the CDN in one wave. It calls the same install path as
`hound update` rather than a second implementation that could drift.

**Release notifications, signed.** dl.houndav.com/latest.json carries
an Ed25519 signature from the same key as the definition packs, for the
same reason: whoever serves that host must not be able to invent a
version and point our users at a binary of their choosing. A manifest
that does not verify is discarded, and no manifest means "nothing newer
known" — never "update available". The safe default is silence.

The signature covers a canonical byte form defined identically in
release.rs and tools/publish-release.py. Signing a re-serialisation of
a parsed struct is the classic way to verify one thing and act on
another, so a test runs a real manifest produced by the Python tool
through the Rust verifier against the production key. If those drift,
manifests verify nowhere and the only symptom is that nobody ever hears
about an update.

Version comparison is numeric, not lexical, because "0.9.0" > "0.10.0"
as strings and that is precisely the pair where it would first be
noticed. Downgrades are never offered — a signed-but-old manifest must
not become a way to reintroduce a fixed vulnerability.

The app does not replace its own binary. A root daemon that can rewrite
itself is the mechanism a supply-chain attacker most wants. Installing
goes through apt with the user present and authenticating: the package
is downloaded, checked against the manifest's SHA-256, staged into a
root-owned 0700 directory so nothing can substitute it between the
check and the read, and handed over. Two guards beyond the signature —
the URL must be on our own download host, because a signature proves
publisher intent and not that the publisher got the URL right.

Tray, per Joe's design: a new amber "attention" state ranked below a
threat and an in-flight scan, above protected. Deeper amber than
"scanning" so the two are distinguishable — scanning lasts seconds and
the user started it, attention persists. The tooltip carries the reason
rather than only that there is one, and "Install Hound X…" appears
enabled only when there is something to install; a permanently greyed
item teaches people the menu is decorative.

Definition staleness is a first-class signal, not just update
availability: amber at 7 days, and at 30 the wording stops pretending —
"this machine is not currently protected against anything found since
then". An antivirus showing green on month-old definitions is lying in
the same way a dead front-end showing "Protected" was.

Also: the four files that declare a version now have a test asserting
they agree. Drift there means a release looks older than what is
installed and the update silently never offers, or offers forever.

377 tests pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 11:58:15 -05:00
dev
3b3586b60a gui: make the desktop app actually launch, and let it reach the daemon
The start-menu entry ran `hound` with Terminal=true — the CLI, which
printed help and exited. No GUI binary had ever been built or packaged.
Four separate faults were stacked behind that report:

1. build-deb.sh now builds and ships hound-gui, and writes a .desktop
   entry only when that binary exists. A launcher for software that is
   not there is worse than no launcher.

2. Tray icons were loaded from a relative "icons/" path, which resolves
   only from the build tree. Installed to /usr/bin the setup hook failed
   and Tauri panicked before a window appeared. They are include_bytes!
   now — four ~1 KB PNGs that can no longer be missing.

3. The front-end never ran at all. app.js opened with a bare module
   specifier ("@tauri-apps/api/core") and there is no bundler, so the
   webview could not resolve it and the script silently failed to parse.
   The window rendered its static HTML forever, which looks exactly like
   a daemon that never answered. withGlobalTauri + window.__TAURI__.

4. build-deb.sh ran the Tauri build as `>/dev/null 2>&1 || true`, so a
   config error scrolled past unseen and the package shipped the
   PREVIOUS binary. Two fixes appeared to do nothing. That step is no
   longer silenced or tolerant of failure, and the build fails outright
   on a bare import in gui/dist/*.js.

Guards, because each of these failed quietly: index.html flips to an
interface-error message if app.js never sets a boot flag within 5s. An
antivirus showing "Protected - your system looks healthy" while its own
front-end is dead is the worst failure mode there is.

Then the window came up and could not reach the daemon: the socket was
0700 root:root. Widening it needed more than a chmod, because
quarantine.restore writes files back out as root — handing that to a
desktop group would hand out root. So the daemon now checks SO_PEERCRED
per method (crates/houndd/src/peer.rs):

  - group `hound`: status, settings.get, events, quarantine.list,
    rootkit.scan, persistence.scan
  - scan/supply.sweep: only paths the caller could read itself, decided
    by forking a child, dropping to the peer's uid, gid and
    supplementary groups, and asking access(2) — which honours ACLs and
    mount options, unlike anything reconstructed from mode bits
  - everything that writes: root, or the uid the daemon runs as

Unclassified methods fall into Admin, so a new mutating method fails
closed rather than becoming public by omission. The end-to-end socket
test caught that "root only" broke every developer run; the owner
clause collapses to "root" under the packaged root daemon and is
verified to do so.

CAP_SETUID/CAP_SETGID join the gate capability set for the readability
check. There was a test asserting CAP_SETUID must never be retained —
it is updated with the reasoning rather than deleted. The daemon
already holds CAP_DAC_OVERRIDE and CAP_DAC_READ_SEARCH, so becoming
another user widens nothing that matters. The unit gains Group=hound so
the socket can be chgrp'd without CAP_CHOWN; it stays uid 0.

Also: the footer claimed "engine: ClamAV via Unix socket". It reports
what the daemon actually loaded, which has been yara-x since the engine
was replaced. Every error path in the front-end goes through explain(),
so a privilege refusal reads as "run it from a terminal: sudo hound …"
rather than "daemon error -32000".

358 tests pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 10:49:58 -05:00
6ef296caa1 Full feature set: realtime monitor, quarantine vault, rootkit scan, settings, events
Daemon (houndd)
- realtime.rs: inotify monitor over watched dirs (default ~/Downloads,
  ~/Documents, ~/Desktop), ClamAV scan on touch, on_detect action
  (quarantine/rename/remove), ransomware heuristic (writes/renames per
  minute above threshold -> 'watching'/'alarm' + critical event)
- quarantine.rs: SHA-256-keyed vault under ~/.local/share/hound/quarantine,
  add/list/restore/remove with original-path metadata
- rootkit.rs: setuid anomaly detection (allowlisted stock binaries),
  deleted-but-executing inodes, world-writable /usr /bin; 3 severity levels
- settings.rs: persisted ~/.config/hound/settings.json, hot-reload on set
- events.rs: ring buffer of severity-tagged events, query + clear

API (hound-api): Settings, Event, QuarantineEntry, RootkitScan/
RootkitFinding, RealtimeStatus types + 10 client methods; Status gains
engine field (engine-agnostic seam)

CLI (hound): events, quarantine list|add|restore|remove, settings
[show|paused|auto-update|notify|realtime on|off|watch|on-detect|
max-size|exclude], rootkit, realtime [status|on|off] — color human
output, --json everywhere

GUI (Tauri 2):
- 16 backend commands bridging every client method
- tray watcher: 1s poll loop, 4-state icon ladder (green/amber/red/gray),
  desktop notification on fresh critical events
- 6-tab frontend: Protection (hero + scan + update), Quarantine (vault
  manager + manual add), Realtime (stats + watch list + toggle), Rootkit
  (on-demand scan), Alerts (event log + clear), Settings (full editor)
- capabilities/default.json for dialog/notification/event permissions

Verified: 27/27 workspace tests, live E2E — EICAR dropped in ~/Downloads
auto-quarantined by the running daemon (critical event logged, file
removed from origin).
2026-08-20 20:33:44 -05:00