feat(audit): chain verify HTTP endpoint

GET /api/audit/verify walks the WORM hash chain and reports whether every
row_hash still matches H(ts||actor||action||detail||prev_hash). Same
verification the CLI --verify-audit-log flag runs, now reachable from the
dashboard so auditors can check integrity without shell access. Response
carries the broken row id in the error message when the chain fails.

Route falls under the existing /api/audit/ prefix so AuthMiddleware
already gates it behind the system:read permission — no middleware
change needed. Uses Arc<dyn AuditRepo> following the fusion explain
endpoint's DDD pattern rather than touching Database directly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
DaLaw2 2026-04-18 18:19:21 +08:00
parent 47d73cb07a
commit 8b0e5d479a

View File

@ -1,10 +1,15 @@
use std::sync::Arc;
use actix_web::{HttpResponse, Scope, web};
use crate::adapter::persistence::Database;
use crate::core::auth::extractor::AuthClaims;
use crate::interface::port::audit::AuditRepo;
pub fn initialize() -> Scope {
web::scope("/audit").route("", web::get().to(list_audit_logs))
web::scope("/audit")
.route("", web::get().to(list_audit_logs))
.route("/verify", web::get().to(verify_chain))
}
async fn list_audit_logs(_auth: AuthClaims, db: web::Data<Database>) -> HttpResponse {
@ -27,3 +32,23 @@ async fn list_audit_logs(_auth: AuthClaims, db: web::Data<Database>) -> HttpResp
Err(_) => HttpResponse::Ok().json(serde_json::json!([])),
}
}
/// `GET /api/audit/verify` — walk the WORM hash chain and report whether
/// every row_hash still matches `H(ts || actor || action || detail ||
/// prev_hash)`. Surfaces over HTTP the same verification the CLI's
/// `--verify-audit-log` flag performs, so auditors can check chain
/// integrity without shell access. Any mismatch returns the offending
/// row id inside `error` so the dashboard can link straight to it.
async fn verify_chain(_auth: AuthClaims, audit: web::Data<Arc<dyn AuditRepo>>) -> HttpResponse {
match audit.verify_audit_log_chain() {
Ok(count) => HttpResponse::Ok().json(serde_json::json!({
"chain_intact": true,
"verified": count,
})),
Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({
"chain_intact": false,
"verified": 0,
"error": e.to_string(),
})),
}
}