Skip to main content

lean_ctx/cli/
audit_report.rs

1use std::collections::HashMap;
2
3use crate::core::audit_trail::{AuditEntry, AuditEventType};
4
5/// `lean-ctx audit evidence --from <rfc3339> --to <rfc3339>
6/// [--framework <id>] [--pack <name|path>] [--out <file>]` —
7/// deterministic, offline-verifiable evidence bundle (GL #425,
8/// `docs/contracts/evidence-bundle-v1.md`; verifier:
9/// `packages/leanctx-verify`).
10pub fn cmd_evidence(args: &[String]) {
11    let flag = |name: &str| -> Option<String> {
12        args.iter()
13            .position(|a| a == name)
14            .and_then(|pos| args.get(pos + 1).cloned())
15    };
16    let (Some(from), Some(to)) = (flag("--from"), flag("--to")) else {
17        eprintln!(
18            "audit evidence: --from and --to (RFC 3339) are required\n\n\
19USAGE:\n  lean-ctx audit evidence --from 2026-05-01T00:00:00Z --to 2026-06-01T00:00:00Z \\\n\
20      [--framework eu-ai-act|iso42001|soc2] [--pack <name|path>] [--out bundle.zip]\n\n\
21Verify without LeanCTX: leanctx-verify <bundle.zip> [--pubkey <hex>]"
22        );
23        std::process::exit(2);
24    };
25
26    let spec = crate::core::evidence_bundle::BundleSpec {
27        from,
28        to,
29        framework: flag("--framework"),
30        pack: flag("--pack"),
31        out: flag("--out").map(std::path::PathBuf::from),
32    };
33    match crate::core::evidence_bundle::generate(&spec) {
34        Ok(result) => {
35            println!("evidence bundle written: {}", result.path.display());
36            println!("bundle sha256: {}", result.sha256);
37            println!("audit entries: {}", result.entries);
38            for f in &result.files {
39                println!("  {f}");
40            }
41            println!(
42                "\nverify offline (no LeanCTX needed):\n  leanctx-verify {}",
43                result.path.display()
44            );
45        }
46        Err(e) => {
47            eprintln!("audit evidence: {e}");
48            std::process::exit(1);
49        }
50    }
51}
52
53pub fn generate_report() -> String {
54    let entries = crate::core::audit_trail::load_recent(10000);
55    let chain = crate::core::audit_trail::verify_chain();
56
57    let mut report = String::new();
58    report.push_str("# lean-ctx Compliance Report\n\n");
59    report.push_str(&format!("Generated: {}\n", chrono::Utc::now().to_rfc3339()));
60    report.push_str(&format!("Audit Trail Entries: {}\n", entries.len()));
61    report.push_str(&format!(
62        "Chain Integrity: {}\n\n",
63        if chain.valid { "VALID" } else { "BROKEN" }
64    ));
65
66    let mut by_agent: HashMap<String, Vec<&AuditEntry>> = HashMap::new();
67    for e in &entries {
68        by_agent.entry(e.agent_id.clone()).or_default().push(e);
69    }
70
71    report.push_str("## Per-Agent Summary\n\n");
72    for (agent, agent_entries) in &by_agent {
73        let tool_calls = agent_entries
74            .iter()
75            .filter(|e| matches!(e.event_type, AuditEventType::ToolCall))
76            .count();
77        let denials = agent_entries
78            .iter()
79            .filter(|e| matches!(e.event_type, AuditEventType::ToolDenied))
80            .count();
81        report.push_str(&format!("### Agent: {agent}\n"));
82        report.push_str(&format!("- Tool calls: {tool_calls}\n"));
83        report.push_str(&format!("- Denials: {denials}\n\n"));
84    }
85
86    let security_events: Vec<_> = entries
87        .iter()
88        .filter(|e| !matches!(e.event_type, AuditEventType::ToolCall))
89        .collect();
90    report.push_str(&format!(
91        "## Security Events ({} total)\n\n",
92        security_events.len()
93    ));
94    for e in security_events.iter().take(50) {
95        report.push_str(&format!(
96            "- [{}] {:?} tool={} agent={}\n",
97            e.timestamp, e.event_type, e.tool, e.agent_id
98        ));
99    }
100
101    report.push_str("\n\n");
102    report.push_str(&crate::core::owasp_alignment::summary());
103
104    report
105}