use super::{clean, stats::SessionStats};
pub fn render(stats: &SessionStats) -> String {
let mut out = String::with_capacity(2048);
out.push_str("# vetto session report\n\n");
out.push_str(&format!(
"- tier: `{}` · net: `{}` · profile: `{}`\n",
markdown_inline(&stats.tier),
markdown_inline(&stats.net_mode),
markdown_inline(&stats.profile)
));
out.push_str(&format!(
"- exit code: `{}` · duration: `{}s`\n\n",
stats.exit_code, stats.duration_secs
));
out.push_str("## Event counts\n\n");
out.push_str("| event | count |\n|---|---|\n");
for (kind, count) in &stats.counts {
out.push_str(&format!("| {} | {count} |\n", markdown_cell(kind)));
}
out.push_str(&format!(
"\nObserved file reads: {} · writes: {} (best-effort /proc polling).\n\n",
stats.file_reads, stats.file_writes
));
out.push_str("## Blocked attempts\n\n");
if stats.blocked_attempts.is_empty() {
out.push_str(
"None observed. Observation channels are optional (see notices); \
enforcement is active regardless.\n\n",
);
} else {
out.push_str("| path | process | source | count |\n|---|---|---|---|\n");
for b in &stats.blocked_attempts {
out.push_str(&format!(
"| {} | {} | {} | {} |\n",
markdown_cell(&b.path),
markdown_cell(&b.comm),
markdown_cell(&b.source),
b.count
));
}
out.push('\n');
}
out.push_str("## Network requests\n\n");
if stats.net_requests.is_empty() {
out.push_str("None (network is off by default).\n\n");
} else {
out.push_str("| host | port | decision |\n|---|---|---|\n");
for r in &stats.net_requests {
let decision = if r.allowed { "allow" } else { "DENIED" };
out.push_str(&format!(
"| {} | {} | {} |\n",
markdown_cell(&r.host),
r.port,
decision
));
}
out.push('\n');
}
out.push_str("## Suspicious signals (best-effort)\n\n");
if stats.suspicious_signals.is_empty() {
out.push_str("None observed. This classifier is advisory and incomplete.\n\n");
} else {
out.push_str("| severity | category | subject | reason | count |\n|---|---|---|---|---|\n");
for signal in &stats.suspicious_signals {
out.push_str(&format!(
"| {} | {} | {} | {} | {} |\n",
markdown_cell(&signal.severity),
markdown_cell(&signal.category),
markdown_cell(&signal.subject),
markdown_cell(&signal.reason),
signal.count
));
}
out.push('\n');
}
out.push_str("## Notices\n\n");
if stats.notices.is_empty() {
out.push_str("- none\n");
} else {
for n in &stats.notices {
out.push_str(&format!("- {}\n", markdown_cell(n)));
}
}
out.push_str(
"\n---\nGenerated by vetto. Observations are BEST-EFFORT and never carry \
enforcement authority. Secret sanitizer: BEST-EFFORT.\n",
);
out
}
fn markdown_cell(value: &str) -> String {
clean(value)
.replace('\\', "\\\\")
.replace('|', "\\|")
.replace('\r', "\\r")
.replace('\n', "\\n")
}
fn markdown_inline(value: &str) -> String {
markdown_cell(value).replace(char::from(96), "'")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn user_strings_are_redacted_and_table_structure_is_escaped() {
let secret = "Bearer abcdefghijklmnop";
let stats = SessionStats {
tier: format!("tier-{secret}"),
net_mode: "off".into(),
profile: "profile".into(),
notices: vec!["row | injected\nnext".into()],
..SessionStats::default()
};
let report = render(&stats);
assert!(
!report.contains("abcdefghijklmnop"),
"secret leaked: {report}"
);
assert!(report.contains("row \\| injected\\nnext"));
}
}