Add bxctl audit verify

Implemented-By: OpenCode session (model recorded in docs/implementer-log.md)
This commit is contained in:
2026-09-21 01:35:08 -07:00
parent 56e5363109
commit 2408e2c622
4 changed files with 283 additions and 4 deletions
+13 -3
View File
@@ -36,9 +36,19 @@ fn main() -> ExitCode {
reason,
} => exit(|| cmd_refuse(&admin_socket, approval, reason.as_deref())),
Command::GrantsCheck { admin_socket } => exit(|| cmd_grants_check(&admin_socket)),
Command::AuditVerify { home: _ } => {
eprintln!("bxctl: audit verify is not implemented yet");
ExitCode::from(1)
Command::AuditVerify { home } => {
let mut out = std::io::stdout().lock();
match bxctl::verify::run(&home, &mut out) {
Ok(true) => ExitCode::SUCCESS,
Ok(false) => ExitCode::from(1),
Err(e) => {
eprintln!(
"bxctl: cannot read the audit log under {}: {e}",
home.display()
);
ExitCode::from(1)
}
}
}
}
}
+90 -1
View File
@@ -1 +1,90 @@
//! Placeholder for the audit-log verifier (task 19).
//! 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();
for entry in std::fs::read_dir(&audit_dir)? {
let entry = entry?;
let name = match entry.file_name().into_string() {
Ok(name) => name,
Err(_) => continue,
};
if is_audit_log(&name) {
names.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")?;
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
)?;
}
Ok(true)
}
/// True if `name` is a `YYYY-MM-DD.jsonl` audit log: ten characters, digits with `-` at positions
/// 4 and 7, then `.jsonl`.
fn is_audit_log(name: &str) -> bool {
let date = match name.strip_suffix(".jsonl") {
Some(date) => date,
None => return false,
};
if date.len() != 10 {
return false;
}
let bytes = date.as_bytes();
for (i, &b) in bytes.iter().enumerate() {
if i == 4 || i == 7 {
if b != b'-' {
return false;
}
} else if !b.is_ascii_digit() {
return false;
}
}
true
}