//! Verifies the whole audit log under a home directory with `bxcore::ChainVerifier`, reading the //! files itself so it works when `brokerd` will not start. use std::io::Write; use std::path::Path; use bxcore::ChainVerifier; /// Verifies the whole audit log under `home` and prints the report to `out`. /// `Ok(true)`: the chain verifies. `Ok(false)`: it does not. `Err`: the log cannot be read. pub fn run(home: &Path, out: &mut dyn Write) -> std::io::Result { let audit_dir = home.join("audit"); let mut names: Vec = Vec::new(); let mut skipped: Vec = Vec::new(); for entry in std::fs::read_dir(&audit_dir)? { let entry = entry?; let name = match entry.file_name().into_string() { Ok(name) => name, Err(raw) => { skipped.push(raw.to_string_lossy().into_owned()); continue; } }; if bxcore::is_audit_log_name(&name) { names.push(name); } else if name.ends_with(".jsonl") { // Looks like a log file but is not named as one: brokerd ignores it too. Say so, so // that nobody takes "ok" to cover it. skipped.push(name); } } names.sort(); let mut verifier = ChainVerifier::new(); for name in &names { let bytes = std::fs::read(audit_dir.join(name))?; verifier.feed(name, &bytes); } let report = verifier.finish(); if let Some(failure) = &report.failure { writeln!(out, "{}:{}: {}", failure.file, failure.line, failure.what)?; writeln!(out, "see docs/runbook.md#audit-chain-broken")?; write_skipped(out, &skipped)?; return Ok(false); } let head = match report.head { Some(hash) => format!("head {}", hash.to_hex()), None => "head none".to_string(), }; writeln!(out, "audit: ok, {} records, {}", report.records, head)?; for loc in &report.recoveries { writeln!(out, "recovered line: {}:{}", loc.file, loc.line)?; } for loc in &report.accepted_breaks { writeln!(out, "accepted break: {}:{}", loc.file, loc.line)?; } for seq in &report.abandoned { writeln!(out, "pending or abandoned: approval {}", seq)?; } for seq in &report.unfinished { writeln!(out, "running or unfinished: decision {}", seq)?; } for loc in &report.clock_warnings { writeln!(out, "clock went backwards: {}:{}", loc.file, loc.line)?; } if let Some(torn) = &report.torn_tail { writeln!( out, "torn final line: {}:{} (brokerd recovers it at its next start)", torn.at.file, torn.at.line )?; } write_skipped(out, &skipped)?; Ok(true) } /// One line per file in `audit/` that looks like a log but is not named as one; neither `brokerd` /// nor this check reads it. Names are escaped: a file name can hold anything. fn write_skipped(out: &mut dyn Write, skipped: &[String]) -> std::io::Result<()> { for name in skipped { writeln!( out, "not an audit log file, not checked: {}", crate::escape::escape_json_text(name) )?; } Ok(()) }