//! Build, gate and sign the Hound Linux threat pack. //! //! build-rules-pack [created] //! //! Three things happen, in this order, and any one of them failing aborts //! the build: //! //! 1. **Compile.** The YARA source has to compile under the exact //! engine the agent runs (yara-x), or a broken pack would take down //! every agent that loaded it — the daemon skips a pack it cannot //! compile, so a bad pack is a silent loss of detection. //! //! 2. **Goodware gate.** Every rule is scanned against /usr/bin, /bin //! and /usr/sbin on this machine. One hit on a system binary and the //! build fails — a rule that quarantines a real binary is worse than //! no rule at all, and the whole product dies the first time it eats //! somebody's `sudo`. This is the same gate as the built-in pack's //! unit test, run here before anything is signed. //! //! 3. **Sign.** Only a pack that compiled and passed the gate is wrapped //! in the signed envelope and written out, so nothing unverifiable or //! untested ever reaches the feed. //! //! The signing key is loaded from the key file (never generated here — a //! threat pack signed by a throwaway key is a threat pack no agent trusts). use ed25519_dalek::SigningKey; use hound_defs::{pack, RulesPack}; fn die(msg: impl std::fmt::Display) -> ! { eprintln!("build-rules-pack: {msg}"); std::process::exit(1); } fn load_key(path: &str) -> SigningKey { let bytes = std::fs::read(path).unwrap_or_else(|e| die(format!("reading key {path}: {e}"))); let seed: [u8; 32] = bytes .get(..32) .and_then(|s| s.try_into().ok()) .unwrap_or_else(|| die(format!("{path} is not at least a 32-byte key"))); SigningKey::from_bytes(&seed) } /// Compile the pack together with the built-in rules, exactly as the /// daemon does, and return the compiled ruleset. fn compile(yara: &str) -> yara_x::Rules { let mut compiler = yara_x::Compiler::new(); compiler .add_source(yara_x::SourceCode::from(yara).with_origin("hound-linux.yar")) .unwrap_or_else(|e| die(format!("the pack does not compile:\n{e}"))); compiler.build() } /// Scan the system binaries and abort on any match. Returns how many /// binaries were checked, so the caller can insist the gate was meaningful. fn goodware_gate(rules: &yara_x::Rules) -> usize { let mut scanner = yara_x::Scanner::new(rules); let mut checked = 0usize; let mut failures: Vec = Vec::new(); for dir in ["/usr/bin", "/bin", "/usr/sbin", "/usr/lib", "/lib"] { let Ok(entries) = std::fs::read_dir(dir) else { continue }; for entry in entries.flatten() { let path = entry.path(); let Ok(md) = std::fs::symlink_metadata(&path) else { continue }; if md.is_symlink() || !md.is_file() || md.len() > 32 * 1024 * 1024 { continue; } let Ok(bytes) = std::fs::read(&path) else { continue }; checked += 1; if let Ok(res) = scanner.scan(&bytes) { for m in res.matching_rules() { failures.push(format!("{} -> {}", path.display(), m.identifier())); } } } } if !failures.is_empty() { die(format!( "GOODWARE GATE FAILED — {} false positive(s) on system files:\n {}", failures.len(), failures.join("\n ") )); } checked } fn main() { let args: Vec = std::env::args().skip(1).collect(); if args.len() < 5 { die("usage: build-rules-pack [created]"); } let (src_path, name, out, key_path, version) = (&args[0], &args[1], &args[2], &args[3], &args[4]); let created = args .get(5) .cloned() .unwrap_or_else(|| "1970-01-01T00:00:00Z".into()); if name.is_empty() || name.len() > 64 || !name.chars().all(|c| c.is_ascii_alphanumeric() || matches!(c, '-' | '_')) { die(format!("pack name {name:?} must be a plain [A-Za-z0-9_-] basename")); } let yara = std::fs::read_to_string(src_path) .unwrap_or_else(|e| die(format!("reading {src_path}: {e}"))); let rules = compile(&yara); let rule_count = rules.iter().count(); eprintln!("compiled {rule_count} rule(s)"); let checked = goodware_gate(&rules); if checked < 200 { die(format!( "goodware gate only saw {checked} binaries — run this on a real system so the gate means something" )); } eprintln!("goodware gate: {checked} system files scanned, 0 false positives"); let rp = RulesPack { version: version.clone(), created, name: name.clone(), yara, }; let payload = serde_json::to_vec(&rp).unwrap_or_else(|e| die(format!("encoding: {e}"))); let key = load_key(key_path); let key_id = std::env::var("HOUND_KEY_ID").unwrap_or_else(|_| "hound-2026".into()); let signed = pack::sign_bytes(payload, &key, &key_id); std::fs::write(out, serde_json::to_string(&signed).unwrap()) .unwrap_or_else(|e| die(format!("writing {out}: {e}"))); eprintln!("wrote {out} — {rule_count} rules, version {version}, signed by {key_id}"); }