Antivirus/crates/hound-watch/examples/triage-live.rs
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

38 lines
1.4 KiB
Rust

//! Poll the registries and triage what was just published.
//!
//! cargo run -p hound-watch --example triage-live -- [how-many]
fn main() -> anyhow::Result<()> {
let limit: usize = std::env::args()
.nth(1)
.and_then(|a| a.parse().ok())
.unwrap_or(40);
let mut cursor = hound_watch::Cursor::default();
let releases = hound_watch::poll(&mut cursor)?;
let npm: Vec<_> = releases.iter().filter(|r| r.ecosystem == "npm").collect();
println!("polled {} npm releases; triaging {}", npm.len(), limit.min(npm.len()));
let mut checked = 0;
let mut with_scripts = 0;
let mut findings = 0;
for r in npm.iter().take(limit) {
let meta = match hound_watch::npm_metadata(&r.name) {
Ok(m) => m,
Err(_) => continue, // unpublished between the poll and now, or rate limited
};
checked += 1;
if !meta.install_scripts.is_empty() {
with_scripts += 1;
}
for f in hound_watch::triage(&meta) {
findings += 1;
println!("\n [{}] {}{}", f.severity.as_str(), f.subject, f.explanation);
for (hook, cmd) in &meta.install_scripts {
println!(" {hook}: {}", &cmd[..cmd.len().min(90)]);
}
}
}
println!("\nchecked {checked}, {with_scripts} run install scripts, {findings} finding(s)");
Ok(())
}