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>
This commit is contained in:
dev 2026-08-21 12:52:17 -05:00
parent b8744c6bdf
commit c3417e5f75
15 changed files with 487 additions and 17 deletions

12
Cargo.lock generated
View file

@ -1089,7 +1089,7 @@ dependencies = [
[[package]] [[package]]
name = "hound" name = "hound"
version = "0.1.4" version = "0.1.5"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"clap", "clap",
@ -1104,7 +1104,7 @@ dependencies = [
[[package]] [[package]]
name = "hound-api" name = "hound-api"
version = "0.1.4" version = "0.1.5"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"serde", "serde",
@ -1114,7 +1114,7 @@ dependencies = [
[[package]] [[package]]
name = "hound-defs" name = "hound-defs"
version = "0.1.4" version = "0.1.5"
dependencies = [ dependencies = [
"ed25519-dalek", "ed25519-dalek",
"serde", "serde",
@ -1124,7 +1124,7 @@ dependencies = [
[[package]] [[package]]
name = "hound-mcp" name = "hound-mcp"
version = "0.1.4" version = "0.1.5"
dependencies = [ dependencies = [
"hound-api", "hound-api",
"hound-supply", "hound-supply",
@ -1134,7 +1134,7 @@ dependencies = [
[[package]] [[package]]
name = "hound-supply" name = "hound-supply"
version = "0.1.4" version = "0.1.5"
dependencies = [ dependencies = [
"hound-defs", "hound-defs",
"serde", "serde",
@ -1143,7 +1143,7 @@ dependencies = [
[[package]] [[package]]
name = "houndd" name = "houndd"
version = "0.1.4" version = "0.1.5"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"ed25519-dalek", "ed25519-dalek",

View file

@ -3,7 +3,7 @@ resolver = "2"
members = ["crates/*"] members = ["crates/*"]
[workspace.package] [workspace.package]
version = "0.1.4" version = "0.1.5"
edition = "2021" edition = "2021"
license = "MIT" license = "MIT"
repository = "https://git.joelovestech.com/Hound/Antivirus.git" repository = "https://git.joelovestech.com/Hound/Antivirus.git"

View file

@ -35,6 +35,12 @@ struct Cli {
enum Cmd { enum Cmd {
/// Show engine status (daemon version, engine, signature-DB age) /// Show engine status (daemon version, engine, signature-DB age)
Status, Status,
/// Report what Hound currently cannot see
Selfcheck {
/// Emit machine-readable JSON instead of human text
#[arg(long)]
json: bool,
},
/// Forward one JSON-RPC request from stdin to the daemon (internal). /// Forward one JSON-RPC request from stdin to the daemon (internal).
/// ///
/// The desktop app cannot perform administrative actions itself — the /// The desktop app cannot perform administrative actions itself — the
@ -410,6 +416,39 @@ fn client(sock: &Option<String>) -> Result<Client> {
/// Returns the process exit code. /// Returns the process exit code.
fn run(client: &Client, cmd: &Cmd) -> Result<i32> { fn run(client: &Client, cmd: &Cmd) -> Result<i32> {
match cmd { match cmd {
Cmd::Selfcheck { json } => {
let v = client.raw_call("selfcheck", None)?;
if *json {
println!("{}", serde_json::to_string_pretty(&v)?);
} else {
let checks = v.get("checks").and_then(|c| c.as_array()).cloned().unwrap_or_default();
for c in &checks {
let state = c.get("state").and_then(|x| x.as_str()).unwrap_or("?");
let id = c.get("id").and_then(|x| x.as_str()).unwrap_or("?");
let detail = c.get("detail").and_then(|x| x.as_str()).unwrap_or("");
let mark = match state {
"ok" => "".green().bold().to_string(),
"degraded" => "!".yellow().bold().to_string(),
_ => "".red().bold().to_string(),
};
println!("{mark} {:<16} {detail}", id.dimmed());
}
let blind = v.get("blind").and_then(|x| x.as_u64()).unwrap_or(0);
let degraded = v.get("degraded").and_then(|x| x.as_u64()).unwrap_or(0);
println!();
if blind == 0 && degraded == 0 {
println!("{} Hound can see everything it checks for", "".green().bold());
} else {
println!(
"{} {blind} blind spot(s), {degraded} degraded — the results above are \
what Hound cannot currently tell you",
"!".yellow().bold()
);
}
}
let blind = v.get("blind").and_then(|x| x.as_u64()).unwrap_or(0);
Ok(if blind > 0 { 1 } else { 0 })
}
Cmd::AdminRpc => { Cmd::AdminRpc => {
use std::io::Read as _; use std::io::Read as _;
let mut line = String::new(); let mut line = String::new();
@ -865,9 +904,100 @@ fn install_app_update(version: &str, deb_url: &str, sha256: &str, assume_yes: bo
anyhow::bail!("the package manager refused the update (staged at {})", path.display()); anyhow::bail!("the package manager refused the update (staged at {})", path.display());
} }
let _ = std::fs::remove_file(&path); let _ = std::fs::remove_file(&path);
restart_desktop_apps();
Ok(true) Ok(true)
} }
/// Restart any running desktop app so it picks up the new binary.
///
/// The app can notice its own package being replaced and reopen itself, but
/// only from the version that learned how — updating *from* an older one
/// leaves the old process running the old front-end, which looks exactly like
/// an update that did nothing. Doing it here works regardless of which
/// version was running.
///
/// We are root and the app is not, so each process is relaunched as its own
/// owner, with the session environment it was already using. Guessing DISPLAY
/// would break on Wayland, a second seat, or a non-standard bus address; the
/// running process already knows the right answer, so take it from there.
fn restart_desktop_apps() {
use std::os::unix::process::CommandExt as _;
let Ok(entries) = std::fs::read_dir("/proc") else {
return;
};
for entry in entries.flatten() {
let Ok(pid) = entry.file_name().to_string_lossy().parse::<u32>() else {
continue;
};
let base = entry.path();
// Only our own GUI, matched on the executable rather than a command
// line anyone could imitate.
match std::fs::read_link(base.join("exe")) {
Ok(exe) if exe.file_name().map(|n| n == "hound-gui").unwrap_or(false) => {}
_ => continue,
}
let Ok(environ) = std::fs::read(base.join("environ")) else {
continue;
};
let session: Vec<(String, String)> = environ
.split(|b| *b == 0)
.filter_map(|kv| std::str::from_utf8(kv).ok())
.filter_map(|kv| kv.split_once('='))
.filter(|(k, _)| {
matches!(
*k,
"DISPLAY"
| "WAYLAND_DISPLAY"
| "XAUTHORITY"
| "DBUS_SESSION_BUS_ADDRESS"
| "XDG_RUNTIME_DIR"
| "XDG_SESSION_TYPE"
| "HOME"
| "PATH"
)
})
.map(|(k, v)| (k.to_string(), v.to_string()))
.collect();
// Who owns it — we must not relaunch someone's desktop app as root.
let Ok(status) = std::fs::read_to_string(base.join("status")) else {
continue;
};
let Some((uid, gid)) = status.lines().find_map(|l| {
let real = l.strip_prefix("Uid:")?.split_whitespace().next()?.parse::<u32>().ok()?;
Some((real, 0u32))
}) else {
continue;
};
let gid = status
.lines()
.find_map(|l| l.strip_prefix("Gid:")?.split_whitespace().next()?.parse::<u32>().ok())
.unwrap_or(gid);
if uid == 0 {
continue; // not a desktop session we should be resurrecting
}
// SAFETY: kill with SIGTERM asks the process to exit; it cannot
// corrupt anything, and the app holds no unsaved state — the window
// is a view over the daemon.
unsafe { libc::kill(pid as libc::pid_t, libc::SIGTERM) };
let mut cmd = std::process::Command::new("/usr/bin/hound-gui");
cmd.env_clear().envs(session).uid(uid).gid(gid);
cmd.stdin(std::process::Stdio::null())
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null());
// Give the old process a moment to release the tray icon, or the
// panel can end up showing two.
std::thread::sleep(std::time::Duration::from_millis(600));
match cmd.spawn() {
Ok(_) => println!(" reopened the Hound window for uid {uid}"),
Err(e) => eprintln!(" could not reopen the Hound window: {e}"),
}
}
}
fn print_update_human(u: &UpdateResult) { fn print_update_human(u: &UpdateResult) {
if u.ok { if u.ok {
println!( println!(
@ -1064,6 +1194,7 @@ mod tests {
use clap::Parser; use clap::Parser;
for argv in [ for argv in [
vec!["hound", "status"], vec!["hound", "status"],
vec!["hound", "selfcheck"],
vec!["hound", "scan", "/tmp"], vec!["hound", "scan", "/tmp"],
vec!["hound", "update"], vec!["hound", "update"],
vec!["hound", "update", "--check"], vec!["hound", "update", "--check"],

View file

@ -32,6 +32,15 @@ pub trait ScanEngine: Send + Sync {
/// "engine online/offline" signal. /// "engine online/offline" signal.
fn probe(&self) -> (bool, String, Option<DbFile>); fn probe(&self) -> (bool, String, Option<DbFile>);
/// How many detection rules are actually compiled and live.
///
/// The self-check needs this to tell "scanned and found nothing" apart
/// from "scanned with no rules loaded", which look identical from the
/// outside and mean opposite things.
fn rule_count(&self) -> usize {
0
}
/// Scan `path` (canonicalize first) and return per-file findings. /// Scan `path` (canonicalize first) and return per-file findings.
fn scan(&self, path: &str, recursive: bool) -> Result<ScanResult>; fn scan(&self, path: &str, recursive: bool) -> Result<ScanResult>;

View file

@ -53,6 +53,7 @@ mod quarantine;
mod realtime; mod realtime;
mod release; mod release;
mod rootkit; mod rootkit;
mod selfcheck;
mod rules; mod rules;
mod settings; mod settings;
mod update; mod update;
@ -107,6 +108,18 @@ fn main() -> Result<()> {
open_socket_to_hound_group(&sock_path); open_socket_to_hound_group(&sock_path);
let state = DaemonState::boot(); let state = DaemonState::boot();
// Announce any blindness at startup rather than letting it be inferred
// from wrong answers later. Every serious bug found in desktop testing
// was Hound reporting success it had not achieved.
let sc = selfcheck::run(&state.defs, engine::engine().rule_count());
for c in sc.checks.iter().filter(|c| c.state != "ok") {
eprintln!("selfcheck [{}]: {}{}", c.state, c.id, c.detail);
}
if sc.healthy() {
eprintln!("selfcheck: {} check(s) passed", sc.ok);
}
start_scheduler(state.clone()); start_scheduler(state.clone());
eprintln!( eprintln!(
@ -535,6 +548,10 @@ fn dispatch(req: &hound_api::Request, st: &DaemonState) -> Result<Value> {
// A fresh, verified look at the release manifest. The scheduler // A fresh, verified look at the release manifest. The scheduler
// checks daily; someone who typed `hound update` is asking now, and // checks daily; someone who typed `hound update` is asking now, and
// "your daily check has not run yet" is not an answer. // "your daily check has not run yet" is not an answer.
"selfcheck" => Ok(serde_json::to_value(selfcheck::run(
&st.defs,
engine::engine().rule_count(),
))?),
"release.check" => { "release.check" => {
let keys = defs::trusted_keys(); let keys = defs::trusted_keys();
let trusted: Vec<(&str, ed25519_dalek::VerifyingKey)> = let trusted: Vec<(&str, ed25519_dalek::VerifyingKey)> =
@ -859,7 +876,7 @@ static KNOWN_RELEASE: std::sync::Mutex<Option<release::Release>> = std::sync::Mu
/// Returns None rather than 0 when the version cannot be parsed. A zero would /// Returns None rather than 0 when the version cannot be parsed. A zero would
/// read as "published today", which is the reassuring answer, and guessing /// read as "published today", which is the reassuring answer, and guessing
/// reassuringly is how a security product ends up lying. /// reassuringly is how a security product ends up lying.
fn defs_age_days(version: &str) -> Option<u32> { pub(crate) fn defs_age_days(version: &str) -> Option<u32> {
let mut parts = version.split(['.', '-']); let mut parts = version.split(['.', '-']);
let y: i32 = parts.next()?.parse().ok()?; let y: i32 = parts.next()?.parse().ok()?;
let m: u8 = parts.next()?.parse().ok()?; let m: u8 = parts.next()?.parse().ok()?;

View file

@ -89,6 +89,10 @@ impl ScanEngine for HoundEngine {
"hound" "hound"
} }
fn rule_count(&self) -> usize {
self.rules.current().count
}
fn probe(&self) -> (bool, String, Option<DbFile>) { fn probe(&self) -> (bool, String, Option<DbFile>) {
let set = self.rules.current(); let set = self.rules.current();
let summary = format!( let summary = format!(

View file

@ -109,7 +109,10 @@ pub fn access_for(method: &str) -> Access {
| "persistence.scan" | "persistence.scan"
// Asking whether a newer version exists changes nothing, and reveals // Asking whether a newer version exists changes nothing, and reveals
// nothing the website does not already say. // nothing the website does not already say.
| "release.check" => Access::Read, | "release.check"
// Reports on this daemon's own blind spots. Reveals nothing about
// the machine that its own operator cannot already see.
| "selfcheck" => Access::Read,
"scan" | "supply.sweep" => Access::ReadsPath, "scan" | "supply.sweep" => Access::ReadsPath,

View file

@ -18,8 +18,27 @@ use hound_api::QuarantineEntry;
use std::path::PathBuf; use std::path::PathBuf;
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
/// Where the quarantine store lives for this user. /// Where the packaged daemon keeps quarantined files. Created 0700 by the
/// installer, on a path no user can execute from by accident.
const SYSTEM_VAULT: &str = "/var/lib/hound/vault";
/// Where the quarantine store lives.
///
/// The system daemon runs as root, and root has a home directory, so the XDG
/// rules below quietly put the vault in /root/.local/share/hound/quarantine.
/// That is wrong twice over: the package creates and hardens
/// /var/lib/hound/vault, which then sat empty, and a desktop app reading the
/// *user's* vault disagreed with the daemon writing root's — which is why the
/// Quarantine tab said "vault is empty" while two quarantined files existed.
///
/// So: running as root means the system vault. Anyone else gets their own,
/// because a user running `houndd` by hand must not need write access to
/// /var/lib.
pub fn store_dir() -> PathBuf { pub fn store_dir() -> PathBuf {
// SAFETY: geteuid takes no arguments and cannot fail.
if unsafe { libc::geteuid() } == 0 {
return PathBuf::from(SYSTEM_VAULT);
}
let data = std::env::var("XDG_DATA_HOME") let data = std::env::var("XDG_DATA_HOME")
.ok() .ok()
.filter(|s| !s.is_empty()); .filter(|s| !s.is_empty());
@ -207,6 +226,25 @@ fn make_id(path: &std::path::Path) -> String {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
/// Root got /root/.local/share/hound/quarantine from the XDG rules, so
/// the vault the installer creates and hardens sat empty while the
/// desktop app — reading the user's own vault — reported it empty too,
/// with quarantined files sitting in neither of the places anyone looked.
#[test]
fn root_quarantines_into_the_system_vault() {
if unsafe { libc::geteuid() } != 0 {
// Non-root must never be sent to /var/lib, which it cannot write.
assert_ne!(store_dir(), std::path::Path::new(SYSTEM_VAULT));
assert!(
store_dir().ends_with("hound/quarantine"),
"an unprivileged daemon keeps its own vault, got {}",
store_dir().display()
);
return;
}
assert_eq!(store_dir(), std::path::Path::new(SYSTEM_VAULT));
}
use super::*; use super::*;
/// Per-test data dir (tag it so tests never share a directory — one /// Per-test data dir (tag it so tests never share a directory — one

View file

@ -0,0 +1,268 @@
//! What can this installation not see?
//!
//! Every serious bug found on the first day of desktop testing had the same
//! shape: Hound reported 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 scanner that called 988 processes
//! hidden when it was the one that had been blinded. A settings write that
//! was 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.
//! So this module asks, deliberately and out loud, what Hound is currently
//! unable to do — and the answers are reported at startup and on demand
//! rather than waiting to be inferred from behaviour.
//!
//! Every check here answers a question with a factual answer. None of them
//! guess, and a check that cannot run says so rather than passing.
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct Check {
/// Stable identifier, for scripts and for suppressing a known-benign one.
pub id: String,
/// "ok" | "degraded" | "blind"
///
/// The third is the one that matters: "blind" means a detector is
/// running and cannot see, which is the state that produces confident
/// wrong answers.
pub state: String,
/// What was actually checked, in a sentence a person can act on.
pub detail: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct SelfCheck {
pub checks: Vec<Check>,
pub ok: u32,
pub degraded: u32,
pub blind: u32,
}
impl SelfCheck {
/// True when something is wrong enough that a green interface would be
/// dishonest.
pub fn healthy(&self) -> bool {
self.blind == 0 && self.degraded == 0
}
fn push(&mut self, id: &str, state: &str, detail: impl Into<String>) {
match state {
"ok" => self.ok += 1,
"degraded" => self.degraded += 1,
_ => self.blind += 1,
}
self.checks.push(Check {
id: id.into(),
state: state.into(),
detail: detail.into(),
});
}
}
/// Run every check. Cheap enough for startup and for a status poll.
pub fn run(defs: &crate::defs::DefsStore, rules_loaded: usize) -> SelfCheck {
let mut r = SelfCheck::default();
check_proc_visibility(&mut r);
check_rules(&mut r, rules_loaded);
check_definitions(&mut r, defs);
check_vault(&mut r);
check_engine(&mut r);
r
}
/// Can this process see other processes?
///
/// `ProtectProc=invisible` in our own systemd unit hid every process from the
/// daemon while `kill(pid, 0)` kept answering, so the hidden-process check
/// reported thousands of rootkits on clean machines. `hidepid=` on the /proc
/// mount does the same thing and is not ours to remove. PID 1 is the control:
/// it always exists, and nothing hides init.
fn check_proc_visibility(r: &mut SelfCheck) {
let listed = crate::rootkit::proc_tids();
if listed.is_empty() {
r.push(
"proc_visibility",
"blind",
"/proc cannot be read at all, so no process check can run",
);
} else if crate::rootkit::pid_exists(1) && !listed.contains(&1) {
r.push(
"proc_visibility",
"blind",
"this daemon's view of /proc is filtered, so hidden-process \
detection cannot run. Check ProtectProc= in the systemd unit or \
hidepid= on the /proc mount",
);
} else {
r.push(
"proc_visibility",
"ok",
format!("{} tasks visible in /proc", listed.len()),
);
}
}
fn check_rules(r: &mut SelfCheck, rules_loaded: usize) {
if rules_loaded == 0 {
r.push(
"rules",
"blind",
"no YARA rules compiled — file scanning cannot detect anything",
);
} else {
r.push("rules", "ok", format!("{rules_loaded} rule(s) compiled"));
}
}
fn check_definitions(r: &mut SelfCheck, defs: &crate::defs::DefsStore) {
let d = defs.current();
if d.indicators == 0 {
r.push(
"definitions",
"blind",
format!(
"no supply-chain indicators loaded{}",
if d.detail.is_empty() {
String::new()
} else {
format!("{}", d.detail)
}
),
);
return;
}
match crate::defs_age_days(&d.version) {
Some(days) if days >= hound_api::DEFS_VERY_STALE_DAYS => r.push(
"definitions",
"degraded",
format!(
"definitions are {days} days old; nothing found since then is detectable"
),
),
Some(days) if days >= hound_api::DEFS_STALE_DAYS => r.push(
"definitions",
"degraded",
format!("definitions are {days} days old"),
),
Some(days) => r.push(
"definitions",
"ok",
format!("{} indicators, {days} day(s) old", d.indicators),
),
None => r.push(
"definitions",
"degraded",
format!(
"{} indicators loaded but the feed version {:?} cannot be dated, \
so staleness is unknown",
d.indicators, d.version
),
),
}
}
/// A vault that cannot be written to means a detection has nowhere to go —
/// and quarantine failing at the moment it matters is not something to
/// discover then.
fn check_vault(r: &mut SelfCheck) {
let dir = crate::quarantine::store_dir();
if !dir.is_dir() {
r.push(
"quarantine",
"degraded",
format!("{} does not exist; it is created on first use", dir.display()),
);
return;
}
let probe = dir.join(".hound-write-probe");
match std::fs::write(&probe, b"") {
Ok(()) => {
let _ = std::fs::remove_file(&probe);
r.push("quarantine", "ok", format!("{} is writable", dir.display()));
}
Err(e) => r.push(
"quarantine",
"blind",
format!(
"{} is not writable ({e}); a detected file could not be quarantined",
dir.display()
),
),
}
}
fn check_engine(r: &mut SelfCheck) {
let (present, summary, _) = crate::engine::engine().probe();
if present {
r.push("engine", "ok", summary);
} else {
r.push("engine", "blind", format!("scanning engine unavailable: {summary}"));
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_clean_result_is_healthy_and_a_blind_one_is_not() {
let mut r = SelfCheck::default();
r.push("a", "ok", "fine");
assert!(r.healthy());
r.push("b", "blind", "cannot see");
assert!(!r.healthy(), "a blind check must not report healthy");
assert_eq!(r.blind, 1);
assert_eq!(r.ok, 1);
}
/// Degraded is not healthy either. Definitions a month old are the case
/// this exists for: everything is technically working and the machine is
/// not protected against anything recent.
#[test]
fn degraded_is_not_healthy() {
let mut r = SelfCheck::default();
r.push("defs", "degraded", "30 days old");
assert!(!r.healthy());
}
/// The check must be able to fail. If this machine's /proc is visible —
/// which it is, or nearly every other test would be failing too — then
/// the visibility check must say ok, and it must count PID 1.
#[test]
fn proc_visibility_passes_on_a_normal_machine() {
let mut r = SelfCheck::default();
check_proc_visibility(&mut r);
assert_eq!(r.checks[0].state, "ok", "{}", r.checks[0].detail);
}
#[test]
fn no_rules_is_reported_as_blind_not_as_a_clean_scan() {
let mut r = SelfCheck::default();
check_rules(&mut r, 0);
assert_eq!(r.checks[0].state, "blind");
let mut r = SelfCheck::default();
check_rules(&mut r, 4);
assert_eq!(r.checks[0].state, "ok");
}
/// Every check must explain itself well enough to act on. An id and a
/// state with no detail is the kind of diagnostic that gets ignored.
#[test]
fn every_check_explains_itself() {
let mut r = SelfCheck::default();
check_proc_visibility(&mut r);
check_rules(&mut r, 4);
check_vault(&mut r);
check_engine(&mut r);
for c in &r.checks {
assert!(!c.id.is_empty(), "a check needs an id");
assert!(
c.detail.len() > 12,
"{} has no useful detail: {:?}",
c.id,
c.detail
);
}
}
}

BIN
dist/hound_0.1.5_amd64.deb vendored Normal file

Binary file not shown.

4
gui/package-lock.json generated
View file

@ -1,12 +1,12 @@
{ {
"name": "hound-gui", "name": "hound-gui",
"version": "0.1.4", "version": "0.1.5",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "hound-gui", "name": "hound-gui",
"version": "0.1.4", "version": "0.1.5",
"dependencies": { "dependencies": {
"@tauri-apps/api": "^2.5.0", "@tauri-apps/api": "^2.5.0",
"@tauri-apps/plugin-dialog": "^2.7.2", "@tauri-apps/plugin-dialog": "^2.7.2",

View file

@ -1,6 +1,6 @@
{ {
"name": "hound-gui", "name": "hound-gui",
"version": "0.1.4", "version": "0.1.5",
"description": "Hound Antivirus — desktop app", "description": "Hound Antivirus — desktop app",
"type": "module", "type": "module",
"scripts": { "scripts": {

View file

@ -1467,7 +1467,7 @@ checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
[[package]] [[package]]
name = "hound-api" name = "hound-api"
version = "0.1.4" version = "0.1.5"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"serde", "serde",
@ -1477,7 +1477,7 @@ dependencies = [
[[package]] [[package]]
name = "hound-gui" name = "hound-gui"
version = "0.1.4" version = "0.1.5"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"hound-api", "hound-api",

View file

@ -1,7 +1,7 @@
[package] [package]
name = "hound-gui" name = "hound-gui"
description = "Hound Antivirus desktop app (Tauri 2)" description = "Hound Antivirus desktop app (Tauri 2)"
version = "0.1.4" version = "0.1.5"
edition = "2021" edition = "2021"
license = "MIT" license = "MIT"
repository = "https://git.joelovestech.com/Hound/Antivirus" repository = "https://git.joelovestech.com/Hound/Antivirus"

View file

@ -1,7 +1,7 @@
{ {
"$schema": "https://schema.tauri.app/config/2", "$schema": "https://schema.tauri.app/config/2",
"productName": "Hound Antivirus", "productName": "Hound Antivirus",
"version": "0.1.4", "version": "0.1.5",
"identifier": "com.joelovestech.hound", "identifier": "com.joelovestech.hound",
"build": { "build": {
"frontendDist": "../dist", "frontendDist": "../dist",