vex/commands/
verify.rs

1//! Verify command - Check audit chain integrity
2//!
3//! Usage:
4//! ```bash
5//! vex verify --audit session.json
6//! vex verify --db vex.db
7//! ```
8
9use anyhow::{Context, Result};
10use clap::Args;
11use colored::Colorize;
12use std::path::{Path, PathBuf};
13
14/// Arguments for the verify command
15#[derive(Args)]
16pub struct VerifyArgs {
17    /// Path to audit JSON file to verify
18    #[arg(long, short = 'a', value_name = "FILE")]
19    audit: Option<PathBuf>,
20
21    /// Path to VEX database file
22    #[arg(long, short = 'd', value_name = "FILE")]
23    db: Option<PathBuf>,
24
25    /// Show detailed verification output
26    #[arg(long)]
27    detailed: bool,
28}
29
30/// Run the verify command
31pub async fn run(args: VerifyArgs) -> Result<()> {
32    if args.audit.is_none() && args.db.is_none() {
33        println!("{}", "VEX Verification".bold().cyan());
34        println!("{}", "═".repeat(40).cyan());
35        println!();
36        println!("Usage:");
37        println!(
38            "  {} Verify an exported audit file",
39            "vex verify --audit session.json".green()
40        );
41        println!(
42            "  {} Verify a VEX database",
43            "vex verify --db vex.db".green()
44        );
45        println!();
46        println!("The verify command checks the integrity of VEX audit chains");
47        println!("using Merkle tree verification.");
48        return Ok(());
49    }
50
51    // Handle audit file verification
52    if let Some(audit_path) = args.audit {
53        verify_audit_file(&audit_path, args.detailed).await?;
54    }
55
56    // Handle database verification
57    if let Some(db_path) = args.db {
58        verify_database(&db_path, args.detailed).await?;
59    }
60
61    Ok(())
62}
63
64/// Verify an exported audit JSON file
65async fn verify_audit_file(path: &std::path::PathBuf, detailed: bool) -> Result<()> {
66    use vex_core::audit::AuditEvent;
67    use vex_core::{Hash, MerkleTree};
68    use vex_persist::audit_store::AuditExport;
69
70    println!("{}", "🔐 VEX Audit Verification".bold().cyan());
71    println!("{}", "═".repeat(40).cyan());
72    println!();
73
74    // Read and parse the audit file
75    let content = std::fs::read_to_string(path)
76        .with_context(|| format!("Failed to read audit file: {}", path.display()))?;
77
78    let audit_data: AuditExport =
79        serde_json::from_str(&content).with_context(|| "Failed to parse audit JSON")?;
80
81    let events = &audit_data.events;
82    let event_count = events.len();
83
84    // Summary info
85    println!("  {} {}", "File:".dimmed(), path.display());
86    println!("  {} {}", "Events:".dimmed(), event_count);
87    println!(
88        "  {} {}",
89        "Merkle Root (File):".dimmed(),
90        audit_data.merkle_root.as_deref().unwrap_or("None")
91    );
92    println!();
93
94    if events.is_empty() {
95        println!("  {}", "Warning: No events to verify".yellow());
96        return Ok(());
97    }
98
99    // 1. Verify individual event hashes and the chain
100    let mut last_hash: Option<Hash> = None;
101    for (i, event) in events.iter().enumerate() {
102        // Re-calculate the "individual" hash (Centralized in vex-core)
103        use vex_core::audit::HashParams;
104        let base_hash = AuditEvent::compute_hash(HashParams {
105            event_type: &event.event_type,
106            timestamp: event.timestamp,
107            sequence_number: event.sequence_number,
108            data: &event.data,
109            actor: &event.actor,
110            rationale: &event.rationale,
111            policy_version: &event.policy_version,
112            data_provenance_hash: &event.data_provenance_hash,
113            human_review_required: event.human_review_required,
114            approval_count: event.approval_signatures.len(),
115        });
116
117        // Calculate expected final hash (including chain link if applicable)
118        let expected_hash = if let Some(prev) = &event.previous_hash {
119            if i == 0 {
120                return Err(anyhow::anyhow!(
121                    "Audit failure: First event has a previous_hash link"
122                ));
123            }
124            if let Some(actual_prev) = &last_hash {
125                if prev != actual_prev {
126                    return Err(anyhow::anyhow!(
127                        "Chain integrity failure at event {}: expected previous_hash {}, got {}",
128                        event.id,
129                        actual_prev.to_hex(),
130                        prev.to_hex()
131                    ));
132                }
133            } else {
134                return Err(anyhow::anyhow!(
135                    "Chain integrity failure: previous_hash present but last_hash missing"
136                ));
137            }
138
139            // Chained hash logic (Centralized in vex-core)
140            AuditEvent::compute_chained_hash(&base_hash, prev, event.sequence_number)
141        } else {
142            if i > 0 {
143                return Err(anyhow::anyhow!(
144                    "Chain integrity failure: Event {} is missing previous_hash link",
145                    event.id
146                ));
147            }
148            base_hash
149        };
150
151        // Compare with the hash in the file
152        if expected_hash != event.hash {
153            return Err(anyhow::anyhow!(
154                "Event hash mismatch at event {}: expected {}, got {}",
155                event.id,
156                expected_hash.to_hex(),
157                event.hash.to_hex()
158            ));
159        }
160
161        last_hash = Some(event.hash.clone());
162    }
163
164    // 2. Build Merkle tree from verified hashes
165    let leaves: Vec<(String, Hash)> = events
166        .iter()
167        .map(|e| (e.id.to_string(), e.hash.clone()))
168        .collect();
169    let tree = MerkleTree::from_leaves(leaves);
170    let calculated_root = tree.root_hash().map(|h| h.to_string());
171
172    // 3. Compare roots
173    match (&audit_data.merkle_root, &calculated_root) {
174        (Some(file_root), Some(calc_root)) => {
175            if file_root != calc_root {
176                return Err(anyhow::anyhow!(
177                    "Merkle root mismatch! File: {}, Calculated: {}",
178                    file_root,
179                    calc_root
180                ));
181            }
182        }
183        (None, None) => {}
184        _ => {
185            return Err(anyhow::anyhow!(
186                "Merkle root presence mismatch between file and calculation"
187            ));
188        }
189    }
190
191    println!(
192        "{} {} verified successfully.",
193        "✓".green().bold(),
194        "Merkle tree & Audit chain".bold()
195    );
196
197    if detailed {
198        println!();
199        println!("{}", "Event Detail Log:".bold());
200        for (i, event) in events.iter().take(10).enumerate() {
201            println!(
202                "  {}. {} [{}] @ {}",
203                i + 1,
204                format!("{:?}", event.event_type).yellow(),
205                event.hash.to_hex()[..8].dimmed(),
206                event.timestamp.to_rfc3339().dimmed()
207            );
208        }
209        if events.len() > 10 {
210            println!("  ... and {} more events", events.len() - 10);
211        }
212    }
213
214    Ok(())
215}
216
217/// Verify a VEX database file
218async fn verify_database(path: &Path, detailed: bool) -> Result<()> {
219    println!("{}", "🔐 VEX Database Verification".bold().cyan());
220    println!("{}", "═".repeat(40).cyan());
221    println!();
222
223    if !path.exists() {
224        println!(
225            "{} Database file not found: {}",
226            "✗".red().bold(),
227            path.display()
228        );
229        std::process::exit(1);
230    }
231
232    println!("  {} {}", "Database:".dimmed(), path.display());
233
234    println!("  {} {}", "Database:".dimmed(), path.display());
235
236    // Connect to database
237    let db_url = format!("sqlite://{}", path.display());
238    let backend = vex_persist::sqlite::SqliteBackend::new(&db_url)
239        .await
240        .with_context(|| format!("Failed to connect to database: {}", path.display()))?;
241
242    let store = vex_persist::audit_store::AuditStore::new(std::sync::Arc::new(backend));
243
244    // In a real multi-tenant system, we'd need a list of tenants.
245    // For the CLI tool, we'll try to find common tenants or verify the default ones.
246    // For now, let's look for all unique tenant_ids in the database.
247    let pool = vex_persist::sqlite::SqliteBackend::new(&db_url)
248        .await?
249        .pool()
250        .clone();
251    let tenants: Vec<String> =
252        sqlx::query_as::<_, (String,)>("SELECT DISTINCT tenant_id FROM audit_events")
253            .fetch_all(&pool)
254            .await
255            .unwrap_or_default()
256            .into_iter()
257            .map(|(t,)| t)
258            .collect();
259
260    if tenants.is_empty() {
261        println!(
262            "  {}",
263            "Warning: No audit events found in database".yellow()
264        );
265        return Ok(());
266    }
267
268    println!("  {} {}", "Tenants found:".dimmed(), tenants.len());
269    println!();
270
271    for tenant in tenants {
272        print!("  Verifying tenant {}... ", tenant.bold());
273        match store.verify_chain(&tenant).await {
274            Ok(true) => println!("{}", "OK".green()),
275            Ok(false) => println!("{}", "FAILED (Integrity break)".red()),
276            Err(e) => println!("{} ({})", "ERROR".red(), e),
277        }
278
279        if detailed {
280            let tree = store.build_merkle_tree(&tenant).await?;
281            println!(
282                "    Merkle Root: {}",
283                tree.root_hash()
284                    .map(|h| h.to_hex())
285                    .unwrap_or_else(|| "None".to_string())
286            );
287            let count = store.get_chain(&tenant).await?.len();
288            println!("    Event Count: {}", count);
289        }
290    }
291
292    println!();
293    println!("{} Database verification complete.", "✓".green().bold());
294
295    Ok(())
296}
297
298#[cfg(test)]
299mod tests {
300    use super::*;
301    use chrono::Utc;
302    use uuid::Uuid;
303    use vex_core::audit::{AuditEvent, AuditEventType};
304    use vex_core::{Hash, MerkleTree};
305    use vex_persist::audit_store::AuditExport;
306
307    fn create_test_audit() -> AuditExport {
308        let agent_id = Uuid::new_v4();
309        let e1 = AuditEvent::new(
310            AuditEventType::AgentCreated,
311            Some(agent_id),
312            serde_json::json!({"role": "Root"}),
313            0,
314        );
315        let e2 = AuditEvent::chained(
316            AuditEventType::AgentExecuted,
317            Some(agent_id),
318            serde_json::json!({"prompt": "Hello"}),
319            e1.hash.clone(),
320            1,
321        );
322        let events = vec![e1.clone(), e2.clone()];
323        let leaves: Vec<(String, Hash)> = events
324            .iter()
325            .map(|e| (e.id.to_string(), e.hash.clone()))
326            .collect();
327        let tree = MerkleTree::from_leaves(leaves);
328
329        AuditExport {
330            events,
331            merkle_root: tree.root_hash().map(|h| h.to_string()),
332            exported_at: Utc::now(),
333            verified: true,
334        }
335    }
336
337    #[tokio::test]
338    async fn test_verify_valid_audit() {
339        let export = create_test_audit();
340        let path = std::env::temp_dir().join("audit_valid.json");
341        let json = serde_json::to_string(&export).unwrap();
342        std::fs::write(&path, json).unwrap();
343
344        let result = verify_audit_file(&path, false).await;
345        assert!(
346            result.is_ok(),
347            "Valid audit should verify! Error: {:?}",
348            result.err()
349        );
350    }
351
352    #[tokio::test]
353    async fn test_verify_tampered_data() {
354        let mut export = create_test_audit();
355        // Tamper with data without updating hash
356        export.events[0].data = serde_json::json!({"role": "TAMPERED"});
357
358        let path = std::env::temp_dir().join("audit_tampered_data.json");
359        let json = serde_json::to_string(&export).unwrap();
360        std::fs::write(&path, json).unwrap();
361
362        let result = verify_audit_file(&path, false).await;
363        assert!(result.is_err());
364        assert!(result
365            .unwrap_err()
366            .to_string()
367            .contains("Event hash mismatch"));
368    }
369
370    #[tokio::test]
371    async fn test_verify_tampered_root() {
372        let mut export = create_test_audit();
373        // Tamper with root
374        export.merkle_root = Some("fake_root".to_string());
375
376        let path = std::env::temp_dir().join("audit_tampered_root.json");
377        let json = serde_json::to_string(&export).unwrap();
378        std::fs::write(&path, json).unwrap();
379
380        let result = verify_audit_file(&path, false).await;
381        assert!(result.is_err());
382        assert!(result
383            .unwrap_err()
384            .to_string()
385            .contains("Merkle root mismatch"));
386    }
387}