A .jsonl file whose name is not a date is ignored by brokerd and by verify; it is now listed, so "ok" does not seem to cover it. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
91 lines
3.1 KiB
Rust
91 lines
3.1 KiB
Rust
//! Verifies the whole audit log under a home directory with `proto::ChainVerifier`, reading the
|
|
//! files itself so it works when `brokerd` will not start.
|
|
|
|
use std::io::Write;
|
|
use std::path::Path;
|
|
|
|
use proto::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 proto::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(())
|
|
}
|