use serde_json::Value;
pub const EXPECTED_TYPE: &str = "dev.cellos.events.cell.evidence_bundle.v1.emitted";
pub const ERROR_SENTINEL_PREFIX: &str = "> **audit_doc: error**";
pub fn render_audit_doc(event: &Value) -> String {
let ty = event.get("type").and_then(|v| v.as_str()).unwrap_or("");
if ty != EXPECTED_TYPE {
return format!(
"{ERROR_SENTINEL_PREFIX} expected event type `{EXPECTED_TYPE}`, got `{ty}`\n"
);
}
let Some(data) = event.get("data") else {
return format!("{ERROR_SENTINEL_PREFIX} event has no `data` payload\n");
};
let mut out = String::new();
let cell_id = str_field(data, "cellId");
let run_id = str_field(data, "runId");
let spec_id = str_field(data, "specId");
let spec_signature_hash = str_field(data, "specSignatureHash");
out.push_str("# CellOS Evidence Bundle — Audit Document\n");
out.push('\n');
out.push_str(&format!("- **cellId**: `{cell_id}`\n"));
out.push_str(&format!("- **runId**: `{run_id}`\n"));
out.push_str(&format!("- **specId**: `{spec_id}`\n"));
out.push_str(&format!(
"- **specSignatureHash**: `{spec_signature_hash}`\n"
));
out.push('\n');
out.push_str("## Lifecycle\n");
out.push('\n');
out.push_str("| # | type | time |\n");
out.push_str("|---|------|------|\n");
if let Some(arr) = data.get("lifecycle").and_then(|v| v.as_array()) {
for (idx, entry) in arr.iter().enumerate() {
let ty = entry
.get("type")
.and_then(|v| v.as_str())
.unwrap_or("unknown");
let time = entry
.get("time")
.and_then(|v| v.as_str())
.unwrap_or("unknown");
out.push_str(&format!("| {} | `{}` | `{}` |\n", idx + 1, ty, time));
}
}
out.push('\n');
out.push_str("## Host Probe Series\n");
out.push('\n');
let host_series = data.get("hostSeries");
for probe in ["fcMetrics", "cgroup", "nftables", "tap"] {
let count = host_series
.and_then(|hs| hs.get(probe))
.map(probe_count)
.unwrap_or_else(|| "unknown".to_string());
out.push_str(&format!("- **{probe}**: {count}\n"));
}
out.push('\n');
out.push_str("## Guest Events\n");
out.push('\n');
let guest_events = data.get("guestEvents").and_then(|v| v.as_array());
let guest_total = guest_events.map(|a| a.len()).unwrap_or(0);
out.push_str(&format!("- **total**: {guest_total}\n"));
out.push_str("- **sample (first 5 types)**:\n");
if let Some(arr) = guest_events {
for entry in arr.iter().take(5) {
let ty = entry
.get("type")
.and_then(|v| v.as_str())
.unwrap_or("unknown");
out.push_str(&format!(" - `{ty}`\n"));
}
}
out.push('\n');
let residue = data
.get("residueClass")
.and_then(|v| v.as_str())
.unwrap_or("unknown");
out.push_str("## Residue Class\n");
out.push('\n');
out.push_str(&format!("- **residueClass**: `{residue}`\n"));
out.push('\n');
out.push_str("## Attestations\n");
out.push('\n');
let attestations = data.get("attestations");
let env_sig = attestations
.and_then(|a| a.get("envelopeSignature"))
.and_then(|v| v.as_str())
.unwrap_or("unknown");
let attest_spec_sig = attestations
.and_then(|a| a.get("specSignatureHash"))
.and_then(|v| v.as_str())
.unwrap_or("unknown");
out.push_str(&format!("- **envelopeSignature**: `{env_sig}`\n"));
out.push_str(&format!("- **specSignatureHash**: `{attest_spec_sig}`\n"));
out
}
fn str_field(data: &Value, key: &str) -> String {
data.get(key)
.and_then(|v| v.as_str())
.unwrap_or("unknown")
.to_string()
}
fn probe_count(probe: &Value) -> String {
if let Some(arr) = probe.as_array() {
return format!("{} sample(s)", arr.len());
}
if let Some(n) = probe.as_u64() {
return format!("{n} sample(s)");
}
if let Some(obj) = probe.as_object() {
if let Some(n) = obj.get("count").and_then(|v| v.as_u64()) {
return format!("{n} sample(s)");
}
if let Some(arr) = obj.get("samples").and_then(|v| v.as_array()) {
return format!("{} sample(s)", arr.len());
}
}
"unknown".to_string()
}