Files
boxmaker/crates/bxctl/src/verify.rs
T
kyleandClaude Opus 5.5 2b3ae080b4 proto becomes bxcore, the one shared crate (M5b task 00)
Done the way the owner asked and the spike mapped: the crate and the
manifests renamed, then what the build reported fixed. Layers: 138
"proto unresolved" errors across 169 files; cargo fmt reordering
imports; six fixture paths built as strings in brokerd's and bxctl's
tests, which the compiler cannot see, and comments naming
crates/proto/; the crate-dependency check and its self-test. One more
the spike did not reach: a `\nproto =` key inside a printf format in
the self-test, which a word-boundary replacement skips because `n` and
`p` are both word characters.

make gate: gate: ok, 944 tests; no new entry in /tmp.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-26 01:48:38 -07:00

91 lines
3.1 KiB
Rust

//! 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<bool> {
let audit_dir = home.join("audit");
let mut names: Vec<String> = Vec::new();
let mut skipped: Vec<String> = 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(())
}