Commit graph

12 commits

Author SHA1 Message Date
dev
79ea89713e Phase 1: license enforcement, threat pack, Apache-2.0, real Action verification
Make the product buyable and the open-source claim true.

Licence system, end to end. license.rs was well-designed dead code; wire
it up: an Ed25519-signed token (same key and verify-before-parse discipline
as definition packs), `hound license install`, houndd loads and verifies at
boot, and the execution gate and full supply-chain feed now gate on
Capability checks. Verification failing always degrades to Free, never to a
locked-out security tool; an expired licence downgrades with the reason
shown. Adds tools/issue-license.py.

Hound Linux threat pack. 34 curated YARA rules — miners, IoT/DDoS bots,
backdoors, rootkits, ransomware, webshells, droppers, reverse shells —
shipped through a new signed rules-pack channel (.rpack) alongside the
definitions feed. Every rule is ELF- or size-anchored and keyed on
family strings, never syscalls; the builder refuses to sign a pack that
matches a system binary (the goodware gate caught two bad rules), and a
regression test proves every rule fires on a sample and stays quiet on a
document about malware.

Action signature verification. The composite action claimed Ed25519
verification "against the same signed manifest the desktop agent uses" but
only compared a same-host sha256. It now fetches latest.json, verifies the
Ed25519 signature over the canonical release statement against the pinned
release key, and installs the checksum from the verified manifest.

Licence resolved to Apache-2.0: Cargo.toml, a real LICENSE file, README.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-21 15:51:33 -05:00
dev
51b8fcd573 0.1.11: one restart mechanism, and a launcher that raises the window
Three tray bugs today, each caused by fixing the previous one without
looking at what else the fix touched. Worth writing down as one thing
rather than three.

**Two dogs, again.** 0.1.4 taught the app to notice its own package
being replaced and reopen. 0.1.5 added the same behaviour to `hound
update`, which is the better mechanism because it works no matter which
version was running — and I did not remove the first. Both fire during
an update: the running app re-execs under a new pid while apt is
working, so the updater kills a pid that has already moved and starts a
second instance beside it.

The in-app restart now waits eight seconds before acting. If the
updater is doing its job this process is terminated during the pause
and never restarts itself; what survives the wait is the only case the
updater cannot cover, which is somebody running `apt upgrade` directly.
Two mechanisms, one arbiter.

**The launcher stopped working.** The single-instance lock added in
0.1.5 to prevent the duplicate icon made a second launch exit quietly,
so clicking the desktop shortcut while Hound was open did nothing at
all — no window, no error, no feedback. It now hands the request to the
running instance, which shows, unminimises and focuses. The same
channel the right-click scan already used: a launch with paths means
"scan these", a launch without means "show yourself".

Also in this release, and the reason the version is what it says it is:
the workspace version did not get bumped. Adding the hound-watch crate
pushed `version =` from line 6 to line 7 of Cargo.toml, and the bump
was a line-numbered sed. The three GUI files went to 0.1.10 and the
workspace stayed at 0.1.9, so the package built as 0.1.9 while the
manifest would have advertised 0.1.10 — every installed agent would
have offered an update forever, installed it, and still seen one
available.

`the_declared_versions_all_agree` caught it before the build finished.
tools/bump-version.sh now does this by matching content and section
rather than line numbers, asserts every substitution actually happened,
and refuses to write invalid JSON.

Adds crates/hound-watch: the registry firehose. npm publishes a CouchDB
_changes stream and PyPI an RSS feed of every new project, both public,
so this needs no user data at all — which is the answer to how a
product with no telemetry grows its detection. Each new package is
handed to the detectors Hound already ships, so there is one definition
of "this install script is hostile" and a rule that fires in CI fires
identically on the firehose. Verified against the live registries: 240
releases in one poll, 60 triaged, parsers tested against captured
responses rather than the network.

476 tests pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 14:53:18 -05:00
dev
46c90b0f59 0.1.9: container config, disabled TLS, and injection inside dependencies
Seven detections, all under the free tier. The line Joe and I settled
on is scope rather than capability: an individual protecting their own
machine gets every check at full depth, and what is sold is the same
protection made continuous, enforced, and shared across a team.

**Containers.** The Docker socket is the one that matters. Mounting
/var/run/docker.sock into a container is not access to Docker, it is
root on the host — anything that can talk to that socket can start a
privileged container with the host filesystem mounted — and the
explanation says exactly that. Also privileged: true, host networking,
bind mounts of /, /etc, ~/.ssh and ~/.aws, secrets baked into image
layers (with the part people learn too late: docker history keeps them
after a later instruction deletes them), curl piped into a shell during
a build nobody watches, ADD from a URL, running as root, and COPY . .
with no .dockerignore shipping the .env and the whole .git directory
into a published image.

**Disabled certificate checking**, across nine ecosystems. Reported as
a warning rather than a critical because it is often deliberate, and
skipped entirely in test files — turning verification off in a fixture
is normal, and flagging it there is how a check gets switched off
wholesale. The explanation leads with the trap: the connection still
looks encrypted.

**Prompt injection inside dependencies.** Hound already read the
project's own CLAUDE.md; an assistant working in a repository reads far
more than that, including the README of every package it touches. All
of that is attacker-controlled text, and publishing a package whose
README addresses the assistant instead of the reader costs nothing to
try. The finding names the package, including scoped ones, and resolves
nested dependencies to the innermost package — the one that actually
shipped the file.

An injected instruction file inside a dependency used to produce two
findings, one from each check. It reports once now, as the dependency
finding: "the package awesome-agents ships an injection" is something a
person can act on, and "there is an injection in this file" is not.

**CI beyond GitHub** — GitLab, Jenkins, CircleCI, Bitbucket, Azure,
Drone. The GitLab equivalent of pull_request_target requires both merge
request pipelines and a protected-variable reference before it reports;
merge request pipelines alone are how everybody uses GitLab, and
flagging them would fire on nearly every project.

Also adds crates/hound-api/src/license.rs: an entitlement check so
nothing ships untiered by accident. It does not try to stop anybody —
the binary is Apache-2.0 and the check can be deleted — and there is no
phone-home, so a machine with no network still knows what it bought and
we never learn where our software runs. An expired licence falls back
to Free rather than failing closed: somebody whose card lapsed must not
end up with less protection than a stranger who installed Hound this
morning, and there is a test for it. Every message about an absent
capability has to name what still works, which is also tested.

466 tests pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 14:37:36 -05:00
dev
0754cf75d0 0.1.8: a GitHub Action, and a light theme that is actually light
**The action.** `hound hygiene` and `hound supply-chain` only ever saw
repositories somebody had already cloned onto a machine with Hound
installed. action/ runs them on every push and pull request: it
installs the published .deb, verifies it against the same signed
checksum the desktop agent uses, and annotates findings on the lines of
the files they concern so a reviewer sees them in the diff rather than
in a log nobody opens.

report.py will not print a credential it found — GitHub masks only
values registered as secrets, so anything else in an annotation is
readable by everyone who can see the run and stays in the API
afterwards. And it will not report a partial scan as clean: a history
walk that hits its limit says so, because "no findings" and "no
findings in the part we looked at" mean different things to somebody
deciding whether to merge.

**The background.** A radial gradient was set on `html, body` — both,
each 100% tall — so it painted twice and the seam between the two
layers appeared as a band across the middle of the page when scrolled.
It was also a hardcoded near-black the light theme had no way to
override. Three more like it: the active tab, button hover, and the log
panel. The ground is a token now, and every colour in the stylesheet
comes from one, so no rule can put one theme's text on the other's
background.

**The palette.** The light theme now uses houndav.com's values exactly
— #5A58C8 buttons, #147A3D, #9A6100, #C22222 — so the app and the site
are recognisably the same product rather than two guesses at it.

Then measured rather than assumed, and found two failures Joe had not
mentioned: "faint" text was 2.90:1 in dark and 3.37:1 in light, and the
dark button hover was 4.41:1. All three now clear 4.5:1, and all eight
text pairs pass WCAG AA in both themes.

**And four more places still naming ClamAV**, which has not been the
engine for a long time: the auto-update caption said the daemon runs
freshclam, the update log said the same, an error suggested `apt
install clamav`, and a permissions hint pointed at /var/lib/clamav. The
engine swap replaced the code and left the copy describing software
this product no longer runs.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 13:40:35 -05:00
dev
aae41a9371 0.1.6: security hygiene, and right-click scanning
Malware scanning asks whether a file is hostile. The check that
actually loses people their accounts is a different one: what has
already been exposed, and what is about to be? crates/hound-supply/src/
hygiene.rs answers it, and runs as part of every project sweep.

  - A secret file tracked by git. The emergency case: it is in the
    history, in every clone, and in every fork. The advice says rotate
    BEFORE `git rm --cached`, because removing a pushed secret does not
    un-share it, and a test asserts that ordering.
  - Credentials hardcoded in source, recognised by issuer format —
    AWS, GitHub, Anthropic, OpenAI, Stripe, Slack, GitLab, npm, PyPI,
    Google, and the PEM private-key headers.
  - Secret files readable by every account on the machine.
  - Secret files with nothing in .gitignore covering them: the near
    miss that the next `git add -A` turns into the emergency above.
  - GitHub Actions: pull_request_target with a checkout of the pull
    request (a stranger's code, your secrets, your write token), a
    secret echoed into the build log, a downloaded script piped into a
    shell, and third-party actions on a moving tag.

Two rules govern all of it. **Findings are actionable**: no entropy
heuristics, because "high entropy string" is a coin flip a human then
has to adjudicate, and people stop reading after the second false
alarm. Every detector recognises a documented credential format or
reports a structural fact that is true or false. **Nothing secret is
copied into a finding** — a report naming the key it found has moved
the key into a log, a CI artefact, or an assistant's context window,
which is the thing being prevented. There is a test for that.

Tracked-file status comes from parsing .git/index rather than running
git: the sweep is pointed at repositories precisely because they are
not trusted, and starting a subprocess inside one is what a hostile
repository wants.

Against a deliberately bad test repository: four critical, five
warnings, and correctly silent on .env.example and actions/checkout@v4
— flagging those is how a scanner gets ignored.

Also in this release:

  - Right-click "Scan for Threats with Hound" in Nemo, Caja and
    Dolphin, whose menu entries are system files. GNOME Files and
    Thunar keep theirs per-user, so `hound context-menu install`
    handles those. The icon is symbolic, so the file manager recolours
    it to the menu's own theme instead of dropping a violet dog into a
    monochrome menu.
  - A right-click while the app is already open hands the request to
    the running instance rather than refusing. A menu item that
    silently does nothing because the app happens to be open is
    indefensible.
  - Two fixes for the duplicate tray icon. The updater slept a fixed
    600ms after SIGTERM and then started the replacement; if the old
    process outlived that, the panel kept its item and the result was
    two dogs, the older of which could not be clicked or closed because
    nothing was behind it. It now waits for the process to actually
    leave /proc, escalating to SIGKILL after five seconds. And the app
    itself now holds an advisory lock for its lifetime, so a second
    instance cannot exist — the kernel releases the lock however the
    process dies, so a stale one is not a state that can happen.

402 tests pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 13:15:35 -05:00
dev
c3417e5f75 0.1.5: say what Hound cannot see, and put the vault where it belongs
Every serious bug found in desktop testing had one shape: Hound
reporting success it had not achieved. A build script that said "built"
without building. A window showing "Protected" while its front-end had
failed to load. A rootkit check calling 988 processes hidden when it
was the one that had been blinded. A settings write refused while the
switch stayed where the user put it. A visible error is something a
person can act on; a false green is not.

`hound selfcheck` asks the question directly — what can this
installation not currently do? — and the daemon prints anything wrong
at startup rather than waiting for it to be inferred from behaviour.
The states are ok, degraded, and blind; the last is the one that
matters, because blind means a detector is running and cannot see. It
exits non-zero when blind, so it can be wired into monitoring.

It earned its place within a minute of existing, by reporting the vault
as /root/.local/share/hound/quarantine. The daemon runs as root, root
has a home directory, and the XDG rules therefore sent the system vault
into root's dotfiles — while the installer created and hardened
/var/lib/hound/vault, which sat empty, and the desktop app read the
user's own vault. Three vaults, none agreeing, which is exactly why the
Quarantine tab reported "vault is empty" beside two quarantined files.
Root now uses the system vault; an unprivileged daemon keeps its own,
since a developer running houndd by hand must not need /var/lib.

Also: `hound update` restarts the desktop app itself after installing.
The app can notice its own package being replaced, but only from the
version that learned how — updating from an older one leaves the stale
process showing the old front-end, which is indistinguishable from an
update that did nothing. The updater matches processes on the
executable rather than a command line anyone could imitate, and
relaunches each as its own owner with the session environment it was
already using: DISPLAY, Wayland socket and bus address are taken from
the running process, because guessing them breaks on Wayland or a
second seat.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 12:52:17 -05:00
dev
b8744c6bdf 0.1.4: settings apply when you change them, and the app reopens itself
Three reports, one shape: the app told the user something had happened
when it had not.

**Appearance was stored in the wrong place.** theme and tray_icon_style
lived in the daemon's settings, which need root to write. So toggling
to a monochrome tray produced a rejected write, no visible change, and
a switch still sitting where it had been put — the screen disagreeing
with reality. Appearance is a per-user preference: it belongs to the
person, not the machine, and putting it in the daemon would also have
made one user's choice everyone's on a shared box. It is client-side
now, applies the instant the control moves, and never prompts for a
password.

**There was a Save button.** A settings screen with one lets you walk
away with your changes discarded, and here it also hid the failure
above. It is gone. Every control writes on change, batched behind a
short delay so toggling three switches is one request and one
authentication prompt rather than three, and typed fields wait for a
pause rather than firing per keystroke. A write the daemon refuses now
reloads the real settings, so a control never keeps a value that was
not accepted.

**The app did not notice being replaced.** After an update the running
process is still the old binary showing the old front-end, which is
indistinguishable from an update that did nothing — the Appearance
panel was in the installed package the whole time and could not be
found without quitting and relaunching. The daemon restarts on upgrade,
so its reported version is the authority on what is installed; when it
stops matching this process's own, the app says so and reopens. One
attempt only, so a version that never matches cannot become a restart
loop.

Also: the tray repaints immediately on an appearance change rather than
waiting for the next poll, and the stored appearance is applied before
first paint so the window does not flash the wrong theme on the way in.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 12:40:59 -05:00
dev
ec1fa4e26f 0.1.3: appearance settings, and stop detecting our own quarantine
A home scan reported two EICAR threats whose paths were Hound's own
vault. SKIP_PREFIXES held the system vault at /var/lib/hound/vault but
not the per-user one under $XDG_DATA_HOME/hound/quarantine, which is a
different absolute path for every user and so cannot be a literal. The
check now matches a `hound` component followed by `quarantine` or
`vault` anywhere in the path, with a test that ordinary paths merely
mentioning either word are still scanned. A scanner that detects its
own evidence locker reports threats that no longer exist anywhere they
can hurt anyone.

Appearance settings, the third pair of fields declared from the start
and wired to nothing (after close_to_tray and auto_update_signatures):

  - Theme: follow system | light | dark, previewing live rather than
    only on save, since a control that does nothing until you press
    another control feels broken.
  - Monochromatic tray icon: one glyph instead of the colour ladder.
    The tooltip still names the state, so only the colour is dropped.

The light theme had to be built — the stylesheet was dark-only. It is
not an inversion: the state colours are darkened until they hold their
contrast on white (the dark theme's green is 2.2:1 there, unreadable as
text) and the neutrals keep a slight violet bias so they read as chosen
rather than as a default grey. One hardcoded near-black on the log
panel would have been near-invisible in light mode; it is a token now.

Which monochrome tone to draw depends on the panel, and no portable way
exists to ask a panel what colour it is — so the webview resolves the
theme (including "follow system", which only prefers-color-scheme can
answer) and tells the tray. One place decides which theme is showing.

The tray now announces a release once per version rather than once per
poll, and its menu entry reads "Update available — install Hound X…".

Also removes ~120 lines: the GUI had its own download, hash-check and
staging implementation for updates. `hound update` does exactly that,
as root, and is the path with tests behind it, so the app runs it under
pkexec instead. That left `hound stage-update` with no caller, and dead
privileged code paths are liabilities, so it is gone.

Verified end to end: 0.1.2 discovered 0.1.3 and installed it
unattended. 380 tests pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 12:28:05 -05:00
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