1use anyhow::{Context, Result};
10use clap::Args;
11use colored::Colorize;
12use std::path::{Path, PathBuf};
13
14#[derive(Args)]
16pub struct VerifyArgs {
17 #[arg(long, short = 'a', value_name = "FILE")]
19 audit: Option<PathBuf>,
20
21 #[arg(long, short = 'd', value_name = "FILE")]
23 db: Option<PathBuf>,
24
25 #[arg(long)]
27 detailed: bool,
28}
29
30pub 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 if let Some(audit_path) = args.audit {
53 verify_audit_file(&audit_path, args.detailed).await?;
54 }
55
56 if let Some(db_path) = args.db {
58 verify_database(&db_path, args.detailed).await?;
59 }
60
61 Ok(())
62}
63
64async 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 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 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 let mut last_hash: Option<Hash> = None;
101 for (i, event) in events.iter().enumerate() {
102 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 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 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 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 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 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
217async 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 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 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 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 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}