diff --git a/Cargo.lock b/Cargo.lock index 348083a..a8d5416 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1089,7 +1089,7 @@ dependencies = [ [[package]] name = "hound" -version = "0.1.2" +version = "0.1.3" dependencies = [ "anyhow", "clap", @@ -1104,7 +1104,7 @@ dependencies = [ [[package]] name = "hound-api" -version = "0.1.2" +version = "0.1.3" dependencies = [ "anyhow", "serde", @@ -1114,7 +1114,7 @@ dependencies = [ [[package]] name = "hound-defs" -version = "0.1.2" +version = "0.1.3" dependencies = [ "ed25519-dalek", "serde", @@ -1124,7 +1124,7 @@ dependencies = [ [[package]] name = "hound-mcp" -version = "0.1.2" +version = "0.1.3" dependencies = [ "hound-api", "hound-supply", @@ -1134,7 +1134,7 @@ dependencies = [ [[package]] name = "hound-supply" -version = "0.1.2" +version = "0.1.3" dependencies = [ "hound-defs", "serde", @@ -1143,7 +1143,7 @@ dependencies = [ [[package]] name = "houndd" -version = "0.1.2" +version = "0.1.3" dependencies = [ "anyhow", "ed25519-dalek", diff --git a/Cargo.toml b/Cargo.toml index 038a9d9..f794998 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,7 @@ resolver = "2" members = ["crates/*"] [workspace.package] -version = "0.1.2" +version = "0.1.3" edition = "2021" license = "MIT" repository = "https://git.joelovestech.com/Hound/Antivirus.git" diff --git a/assets/icons/state-mono-dark-16.png b/assets/icons/state-mono-dark-16.png new file mode 100644 index 0000000..d27a4f1 Binary files /dev/null and b/assets/icons/state-mono-dark-16.png differ diff --git a/assets/icons/state-mono-dark-22.png b/assets/icons/state-mono-dark-22.png new file mode 100644 index 0000000..cf96f65 Binary files /dev/null and b/assets/icons/state-mono-dark-22.png differ diff --git a/assets/icons/state-mono-dark-24.png b/assets/icons/state-mono-dark-24.png new file mode 100644 index 0000000..40717c5 Binary files /dev/null and b/assets/icons/state-mono-dark-24.png differ diff --git a/assets/icons/state-mono-dark-32.png b/assets/icons/state-mono-dark-32.png new file mode 100644 index 0000000..633603b Binary files /dev/null and b/assets/icons/state-mono-dark-32.png differ diff --git a/assets/icons/state-mono-dark-48.png b/assets/icons/state-mono-dark-48.png new file mode 100644 index 0000000..9095011 Binary files /dev/null and b/assets/icons/state-mono-dark-48.png differ diff --git a/assets/icons/state-mono-light-16.png b/assets/icons/state-mono-light-16.png new file mode 100644 index 0000000..77cc794 Binary files /dev/null and b/assets/icons/state-mono-light-16.png differ diff --git a/assets/icons/state-mono-light-22.png b/assets/icons/state-mono-light-22.png new file mode 100644 index 0000000..c6d872b Binary files /dev/null and b/assets/icons/state-mono-light-22.png differ diff --git a/assets/icons/state-mono-light-24.png b/assets/icons/state-mono-light-24.png new file mode 100644 index 0000000..429e66e Binary files /dev/null and b/assets/icons/state-mono-light-24.png differ diff --git a/assets/icons/state-mono-light-32.png b/assets/icons/state-mono-light-32.png new file mode 100644 index 0000000..31620bb Binary files /dev/null and b/assets/icons/state-mono-light-32.png differ diff --git a/assets/icons/state-mono-light-48.png b/assets/icons/state-mono-light-48.png new file mode 100644 index 0000000..9826769 Binary files /dev/null and b/assets/icons/state-mono-light-48.png differ diff --git a/crates/hound/src/main.rs b/crates/hound/src/main.rs index 1fa994f..5e20ad3 100644 --- a/crates/hound/src/main.rs +++ b/crates/hound/src/main.rs @@ -48,15 +48,6 @@ enum Cmd { /// privilege. #[command(hide = true)] AdminRpc, - /// Write stdin to a staged update file (internal). - /// - /// The desktop app downloads a published package and checks it against - /// the signed manifest, then needs it somewhere root-owned so nothing can - /// substitute the file between that check and the package manager reading - /// it. Deliberately narrow: one fixed directory, one filename shape, and - /// no way to name a path outside it. - #[command(hide = true)] - StageUpdate { path: String }, /// Scan a file or directory Scan { /// Path to scan (file or directory) @@ -419,39 +410,6 @@ fn client(sock: &Option) -> Result { /// Returns the process exit code. fn run(client: &Client, cmd: &Cmd) -> Result { match cmd { - Cmd::StageUpdate { path } => { - use std::io::Read as _; - const STAGE_DIR: &str = "/var/lib/hound/updates"; - let p = std::path::Path::new(path); - // The caller is unprivileged and we are not; treat the path as - // hostile. It must sit directly in the staging directory and be - // named like a package — no traversal, no subdirectories, no - // writing over anything else on the system. - let name = p - .file_name() - .and_then(|n| n.to_str()) - .ok_or_else(|| anyhow::anyhow!("no filename"))?; - if p.parent() != Some(std::path::Path::new(STAGE_DIR)) - || !name.ends_with(".deb") - || name.contains('/') - || name.starts_with('.') - { - anyhow::bail!("a staged update must be a .deb directly in {STAGE_DIR}"); - } - std::fs::create_dir_all(STAGE_DIR)?; - std::fs::set_permissions( - STAGE_DIR, - std::os::unix::fs::PermissionsExt::from_mode(0o700), - )?; - let mut body = Vec::new(); - // A package is tens of megabytes; this bound is generous and - // still cannot fill a disk. - std::io::stdin().take(256 << 20).read_to_end(&mut body)?; - std::fs::write(p, &body)?; - std::fs::set_permissions(p, std::os::unix::fs::PermissionsExt::from_mode(0o600))?; - println!("staged {} bytes", body.len()); - Ok(0) - } Cmd::AdminRpc => { use std::io::Read as _; let mut line = String::new(); diff --git a/crates/houndd/src/native.rs b/crates/houndd/src/native.rs index 1754709..c28d737 100644 --- a/crates/houndd/src/native.rs +++ b/crates/houndd/src/native.rs @@ -265,12 +265,74 @@ impl HoundEngine { } /// Pseudo-filesystems and our own vault: never walked. +/// +/// The per-user vault lives under `$XDG_DATA_HOME/hound/quarantine`, which is +/// a different absolute path for every user, so it cannot be a literal in +/// SKIP_PREFIXES. Scanning it means re-detecting every threat already caught +/// — a home scan reported two EICAR hits whose paths were Hound's own vault, +/// which is alarming, useless, and would re-quarantine files already +/// quarantined. fn is_skipped_dir(path: &Path) -> bool { - SKIP_PREFIXES.iter().any(|p| path.starts_with(p)) + if SKIP_PREFIXES.iter().any(|p| path.starts_with(p)) { + return true; + } + is_quarantine_path(path) +} + +/// Any user's quarantine vault, wherever their data directory happens to be. +fn is_quarantine_path(path: &Path) -> bool { + let mut parts = path.components().peekable(); + while let Some(c) = parts.next() { + if c.as_os_str() == "hound" { + if let Some(next) = parts.peek() { + if next.as_os_str() == "quarantine" || next.as_os_str() == "vault" { + return true; + } + } + } + } + false } #[cfg(test)] mod tests { + /// Hound quarantined two EICAR files, then a home scan found them again + /// — in its own vault — and reported the machine as infected. A scanner + /// that detects its own evidence locker reports a threat that no longer + /// exists anywhere it can hurt anyone. + #[test] + fn the_quarantine_vault_is_never_scanned() { + for p in [ + "/home/anon/.local/share/hound/quarantine/3193804faa76105a", + "/home/someone/.local/share/hound/quarantine", + "/var/lib/hound/vault/abc123", + "/root/.local/share/hound/quarantine/x", + ] { + assert!( + is_skipped_dir(std::path::Path::new(p)), + "{p} is a quarantine vault and must not be scanned" + ); + } + } + + /// The check keys on "hound" followed by "quarantine" or "vault", so it + /// must not swallow ordinary directories that merely mention either word. + #[test] + fn ordinary_paths_are_still_scanned() { + for p in [ + "/home/anon/projects/hound/src/main.rs", + "/home/anon/quarantine-notes.txt", + "/home/anon/hound-vault-design.md", + "/srv/vault/data", + "/home/anon/Downloads/hound.deb", + ] { + assert!( + !is_skipped_dir(std::path::Path::new(p)), + "{p} is a normal path and must still be scanned" + ); + } + } + use super::*; use std::fs; diff --git a/dist/hound_0.1.3_amd64.deb b/dist/hound_0.1.3_amd64.deb new file mode 100644 index 0000000..d727022 Binary files /dev/null and b/dist/hound_0.1.3_amd64.deb differ diff --git a/gui/dist/app.js b/gui/dist/app.js index dff930e..e523cef 100644 --- a/gui/dist/app.js +++ b/gui/dist/app.js @@ -10,6 +10,23 @@ const { listen } = window.__TAURI__.event; const $ = (id) => document.getElementById(id); +/// Paint the chosen theme. "auto" defers to the desktop, which the webview +/// reports through prefers-color-scheme — and which can change while the app +/// is open, so the listener stays attached rather than sampling once. +const SYSTEM_DARK = window.matchMedia("(prefers-color-scheme: dark)"); +let themeChoice = "auto"; +function applyTheme(choice) { + themeChoice = choice || "auto"; + const dark = themeChoice === "dark" || (themeChoice === "auto" && SYSTEM_DARK.matches); + document.documentElement.setAttribute("data-theme", dark ? "dark" : "light"); + // The tray needs this too: which monochrome tone to draw depends on it, + // and only the webview can resolve "follow system". + invoke("set_theme_resolved", { dark }).catch(() => {}); +} +SYSTEM_DARK.addEventListener("change", () => { + if (themeChoice === "auto") applyTheme("auto"); +}); + // The daemon refuses anything that writes unless the caller is root, because // quarantine restores files back out as root and that is not something group // membership should confer. The app runs as you, so those actions need a @@ -419,6 +436,9 @@ async function loadSettings() { $("set-ransom").checked = s.ransomware_guard; $("set-ransom-thresh").value = s.ransomware_threshold_per_min; $("set-rootkit").checked = s.rootkit_enabled; + $("set-theme").value = s.theme || "auto"; + $("set-monochrome").checked = (s.tray_icon_style || "colour") === "monochrome"; + applyTheme(s.theme || "auto"); } catch (e) { $("settings-msg").textContent = "Failed to load: " + explain(e); } @@ -433,6 +453,8 @@ async function saveSettings() { s.realtime_enabled = $("set-realtime").checked; s.realtime_watch = $("set-watch").value.split("\n").map((x) => x.trim()).filter(Boolean); s.on_detect = $("set-ondetect").value; + s.theme = $("set-theme").value; + s.tray_icon_style = $("set-monochrome").checked ? "monochrome" : "colour"; s.max_file_size_mb = Math.max(1, parseInt($("set-maxsize").value, 10) || 0); s.exclude_paths = $("set-excludes").value.split("\n").map((x) => x.trim()).filter(Boolean); s.recursive_default = $("set-recursive").checked; @@ -549,4 +571,6 @@ async function boot() { // Tells the watchdog in index.html that the script actually ran. window.__houndBooted = true; +$("set-theme").addEventListener("change", (e) => applyTheme(e.target.value)); + boot(); diff --git a/gui/dist/index.html b/gui/dist/index.html index 9c9f788..1d3e97b 100644 --- a/gui/dist/index.html +++ b/gui/dist/index.html @@ -197,6 +197,22 @@
+
+

Appearance

+ + +
+

Protection