Antivirus/gui/dist/index.html
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

303 lines
15 KiB
HTML

<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Hound Antivirus</title>
<link rel="stylesheet" href="styles.css" />
<link rel="icon" type="image/png" href="favicon-32.png" />
</head>
<body>
<div id="app">
<!-- ── Header ─────────────────────────────────────────────── -->
<header class="header">
<div class="brand">
<img src="hound-32.png" alt="" class="brand-mark" />
<div class="brand-text">
<h1>Hound</h1>
<span class="tagline">Antivirus for Linux</span>
</div>
</div>
<div class="header-status">
<span class="engine-dot" id="engine-dot" title="Engine status"></span>
<span class="engine-label" id="engine-label">connecting…</span>
</div>
</header>
<!-- ── Tabs ───────────────────────────────────────────────── -->
<nav class="tabs" role="tablist">
<button class="tab active" data-tab="protection" role="tab">
<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg>
Protection
</button>
<button class="tab" data-tab="quarantine" role="tab">
<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 8v13H3V8"/><path d="M1 3h22v5H1z"/><path d="M10 12h4"/></svg>
Quarantine
<span class="tab-badge hidden" id="badge-quarantine"></span>
</button>
<button class="tab" data-tab="realtime" role="tab">
<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M2 12h4l3-9 4 18 3-9h6"/></svg>
Realtime
</button>
<button class="tab" data-tab="rootkit" role="tab">
<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/></svg>
Rootkit
</button>
<button class="tab" data-tab="alerts" role="tab">
<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 8a6 6 0 0 0-12 0c0 7-3 9-3 9h18s-3-2-3-9"/><path d="M13.7 21a2 2 0 0 1-3.4 0"/></svg>
Alerts
<span class="tab-badge hidden" id="badge-alerts"></span>
</button>
<button class="tab" data-tab="settings" role="tab">
<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 1 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 1 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 1 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 1 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg>
Settings
</button>
</nav>
<!-- ══ Protection ═══════════════════════════════════════════ -->
<section class="tab-panel active" id="panel-protection">
<section class="hero" id="hero">
<div class="shield" id="shield">
<img id="shield-icon" src="state-protected-48.png" alt="" width="48" height="48" />
</div>
<div class="hero-copy">
<h2 id="hero-title">Protected</h2>
<p id="hero-sub">Your system looks healthy.</p>
<div class="hero-meta">
<span class="pill" id="pill-db">signatures: —</span>
<span class="pill" id="pill-os"></span>
<span class="pill" id="pill-engine">engine: —</span>
</div>
</div>
</section>
<section class="actions">
<button class="btn primary" id="btn-scan-home">
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><circle cx="11" cy="11" r="7"/><path d="m20 20-3.5-3.5"/></svg>
Scan Home
</button>
<button class="btn primary" id="btn-scan-custom">Scan Folder…</button>
<button class="btn" id="btn-update">Update Signatures</button>
<button class="btn" id="btn-pause">Pause Protection</button>
</section>
<section class="panel hidden" id="scan-panel">
<div class="panel-head">
<h3 id="scan-title">Scanning…</h3>
<span class="scan-target" id="scan-target"></span>
</div>
<div class="progress">
<div class="progress-bar" id="progress-bar"></div>
</div>
<p class="muted" id="scan-note">Working through the queue. This can take a while on large folders.</p>
</section>
<section class="panel" id="results-panel">
<div class="panel-head">
<h3>Last Scan Results</h3>
<span class="muted" id="results-meta"></span>
</div>
<div id="results-body" class="results">
<p class="muted empty">No scans yet. Hit <strong>Scan Home</strong> to start.</p>
</div>
</section>
<section class="panel hidden" id="update-panel">
<div class="panel-head">
<h3>Signature Update</h3>
</div>
<pre class="log" id="update-log"></pre>
</section>
</section>
<!-- ══ Quarantine ═══════════════════════════════════════════ -->
<section class="tab-panel" id="panel-quarantine">
<section class="panel">
<div class="panel-head">
<h3>Quarantine Vault</h3>
<div class="row-actions">
<span class="muted" id="qt-count"></span>
<button class="btn small" id="btn-qt-refresh">Refresh</button>
</div>
</div>
<div id="qt-body" class="qt-list">
<p class="muted empty">Loading…</p>
</div>
</section>
<section class="panel">
<div class="panel-head"><h3>Add Manually</h3></div>
<div class="form-row">
<input type="text" id="qt-add-path" class="input" placeholder="/path/to/suspicious/file" />
<input type="text" id="qt-add-virus" class="input" placeholder="label (e.g. manual-suspicious)" value="manual" />
<button class="btn" id="btn-qt-add">Quarantine</button>
</div>
<p class="muted" style="margin-top:8px">The file is moved into the vault; its original location is left empty.</p>
</section>
</section>
<!-- ══ Realtime ═════════════════════════════════════════════ -->
<section class="tab-panel" id="panel-realtime">
<section class="panel">
<div class="panel-head">
<h3>Realtime Monitor</h3>
<label class="switch-row">
<span id="rt-enabled-label" class="muted">loading…</span>
<input type="checkbox" id="rt-toggle" />
<span class="switch"></span>
</label>
</div>
<div class="stat-grid" id="rt-stats">
<div class="stat"><span class="stat-val" id="rt-seen"></span><span class="stat-key">file events seen</span></div>
<div class="stat"><span class="stat-val" id="rt-quar"></span><span class="stat-key">auto-quarantined</span></div>
<div class="stat"><span class="stat-val" id="rt-uptime"></span><span class="stat-key">monitor uptime</span></div>
<div class="stat"><span class="stat-val" id="rt-ransom"></span><span class="stat-key">ransomware guard</span></div>
</div>
<div class="kv"><span class="kv-k">watching</span><span class="kv-v" id="rt-watch"></span></div>
<div class="kv"><span class="kv-k">last file event</span><span class="kv-v" id="rt-last"></span></div>
<div class="kv"><span class="kv-k">on detection</span><span class="kv-v" id="rt-action"></span></div>
</section>
<section class="panel">
<div class="panel-head"><h3>Watch Folders</h3></div>
<div id="rt-watch-list" class="watch-list"></div>
<p class="muted" style="margin-top:8px">Edit in Settings → Realtime. Restart required for the monitor to pick up new folders.</p>
</section>
</section>
<!-- ══ Rootkit ══════════════════════════════════════════════ -->
<section class="tab-panel" id="panel-rootkit">
<section class="panel">
<div class="panel-head">
<h3>Rootkit Hunter</h3>
<button class="btn primary" id="btn-rootkit">Run Scan</button>
</div>
<div id="rootkit-banner" class="verdict hidden"></div>
<div id="rootkit-body" class="finding-list">
<p class="muted empty">Runs userspace heuristics: deleted-but-open executables, world-writable binaries, unexpected setuid, hidden /proc entries.</p>
</div>
</section>
</section>
<!-- ══ Alerts ═══════════════════════════════════════════════ -->
<section class="tab-panel" id="panel-alerts">
<section class="panel">
<div class="panel-head">
<h3>Event Log</h3>
<div class="row-actions">
<span class="muted" id="alerts-count"></span>
<button class="btn small" id="btn-alerts-refresh">Refresh</button>
<button class="btn small danger" id="btn-alerts-clear">Clear Log</button>
</div>
</div>
<div id="alerts-body" class="event-list">
<p class="muted empty">Loading…</p>
</div>
</section>
</section>
<!-- ══ Settings ═════════════════════════════════════════════ -->
<section class="tab-panel" id="panel-settings">
<section class="panel">
<div class="panel-head"><h3>Protection</h3></div>
<label class="switch-row setting">
<span><strong>Pause all protection</strong><small>Suspends realtime and grays the tray.</small></span>
<input type="checkbox" id="set-paused" /><span class="switch"></span>
</label>
<label class="switch-row setting">
<span><strong>Auto-update signatures</strong><small>Daemon runs freshclam on a schedule.</small></span>
<input type="checkbox" id="set-autoupdate" /><span class="switch"></span>
</label>
<label class="switch-row setting">
<span><strong>Desktop notifications</strong><small>Pop an alert for critical events.</small></span>
<input type="checkbox" id="set-notify" /><span class="switch"></span>
</label>
</section>
<section class="panel">
<div class="panel-head"><h3>Realtime</h3></div>
<label class="switch-row setting">
<span><strong>Realtime monitor</strong><small>Watch folders and quarantine on the fly.</small></span>
<input type="checkbox" id="set-realtime" /><span class="switch"></span>
</label>
<div class="field">
<label for="set-watch">Watch folders (one per line)</label>
<textarea id="set-watch" class="input mono" rows="4" spellcheck="false"></textarea>
</div>
<div class="field">
<label for="set-ondetect">Action on detection</label>
<select id="set-ondetect" class="input">
<option value="quarantine">Quarantine automatically</option>
<option value="alert">Alert only (leave file in place)</option>
</select>
</div>
</section>
<section class="panel">
<div class="panel-head"><h3>Scanning</h3></div>
<div class="field">
<label for="set-maxsize">Max file size (MB)</label>
<input type="number" id="set-maxsize" class="input" min="1" />
</div>
<div class="field">
<label for="set-excludes">Excluded paths (one per line, exact or prefix)</label>
<textarea id="set-excludes" class="input mono" rows="4" spellcheck="false"></textarea>
</div>
<label class="switch-row setting">
<span><strong>Recursive by default</strong></span>
<input type="checkbox" id="set-recursive" /><span class="switch"></span>
</label>
</section>
<section class="panel">
<div class="panel-head"><h3>Ransomware Guard</h3></div>
<label class="switch-row setting">
<span><strong>Enable guard</strong><small>Write-burst heuristic trips an alarm under attack.</small></span>
<input type="checkbox" id="set-ransom" /><span class="switch"></span>
</label>
<div class="field">
<label for="set-ransom-thresh">Threshold (file-writes / minute)</label>
<input type="number" id="set-ransom-thresh" class="input" min="10" />
</div>
</section>
<section class="panel">
<div class="panel-head"><h3>Rootkit</h3></div>
<label class="switch-row setting">
<span><strong>Include in scans</strong><small>Run rootkit heuristics as part of the health check.</small></span>
<input type="checkbox" id="set-rootkit" /><span class="switch"></span>
</label>
</section>
<div class="actions">
<button class="btn primary" id="btn-settings-save">Save Settings</button>
<span class="muted" id="settings-msg"></span>
</div>
</section>
<footer class="footer">
<span id="foot-ver">houndd —</span>
<span class="muted" id="foot-engine">engine —</span>
</footer>
</div>
<script type="module" src="app.js"></script>
<script>
// A front-end that fails to load looks identical to a daemon that never
// answered: the window just sits on its placeholder text. It happened
// once (an unresolvable bare module specifier) and cost real debugging
// time, so failure now announces itself.
setTimeout(function () {
if (window.__houndBooted) return;
var sub = document.getElementById("hero-sub");
if (sub) {
sub.textContent =
"The Hound interface failed to start. This is a bug in the app, not " +
"a problem with your system \u2014 please report it. Scanning still " +
"works from the terminal: hound scan ~/Downloads";
}
var label = document.getElementById("engine-label");
if (label) label.textContent = "interface error";
}, 5000);
</script>
</body>
</html>