tracevault_cli/commands/
verify.rs1use crate::api_client::{resolve_credentials, ApiClient, CiVerifyRequest};
2use crate::config::TracevaultConfig;
3use std::path::Path;
4use std::process::Command;
5
6fn git_repo_name(project_root: &Path) -> String {
7 Command::new("git")
8 .args(["rev-parse", "--show-toplevel"])
9 .current_dir(project_root)
10 .output()
11 .ok()
12 .filter(|o| o.status.success())
13 .map(|o| String::from_utf8_lossy(&o.stdout).trim().to_string())
14 .as_deref()
15 .and_then(|p| p.rsplit('/').next())
16 .map(String::from)
17 .unwrap_or_else(|| "unknown".into())
18}
19
20fn expand_range(
21 project_root: &Path,
22 range: &str,
23) -> Result<Vec<String>, Box<dyn std::error::Error>> {
24 let output = Command::new("git")
25 .args(["rev-list", "--reverse", range])
26 .current_dir(project_root)
27 .output()?;
28
29 if !output.status.success() {
30 let stderr = String::from_utf8_lossy(&output.stderr);
31 return Err(format!("git rev-list failed: {stderr}").into());
32 }
33
34 let shas = String::from_utf8_lossy(&output.stdout)
35 .lines()
36 .map(|l| l.trim().to_string())
37 .filter(|l| !l.is_empty())
38 .collect();
39
40 Ok(shas)
41}
42
43pub async fn verify(
44 project_root: &Path,
45 commits: Option<&str>,
46 range: Option<&str>,
47) -> Result<(), Box<dyn std::error::Error>> {
48 let commit_list = if let Some(range) = range {
49 expand_range(project_root, range)?
50 } else if let Some(commits) = commits {
51 commits
52 .split(',')
53 .map(|s| s.trim().to_string())
54 .filter(|s| !s.is_empty())
55 .collect()
56 } else {
57 return Err("Provide either --commits or --range".into());
58 };
59
60 if commit_list.is_empty() {
61 println!("No commits to verify.");
62 return Ok(());
63 }
64
65 let (server_url, token) = resolve_credentials(project_root);
66
67 let server_url =
68 match server_url {
69 Some(url) => url,
70 None => return Err(
71 "No server URL configured. Set TRACEVAULT_SERVER_URL or run 'tracevault login'."
72 .into(),
73 ),
74 };
75
76 if token.is_none() {
77 return Err("No auth token. Set TRACEVAULT_API_KEY or run 'tracevault login'.".into());
78 }
79
80 let org_slug = TracevaultConfig::load(project_root)
81 .and_then(|c| c.org_slug)
82 .ok_or("No org_slug in config. Run 'tracevault init' first.")?;
83
84 let client = ApiClient::new(&server_url, token.as_deref());
85
86 let repo_name = git_repo_name(project_root);
88 let repos = client.list_repos(&org_slug).await?;
89 let repo = repos.iter().find(|r| r.name == repo_name).ok_or_else(|| {
90 format!(
91 "Repo '{}' not found on server. Run 'tracevault sync' first.",
92 repo_name
93 )
94 })?;
95
96 println!(
97 "Verifying {} commit(s) for repo '{}'...",
98 commit_list.len(),
99 repo_name
100 );
101
102 let result = client
103 .verify_commits(
104 &org_slug,
105 &repo.id,
106 CiVerifyRequest {
107 commits: commit_list,
108 },
109 )
110 .await?;
111
112 println!();
114 for r in &result.results {
115 let icon = match r.status.as_str() {
116 "pass" => "\x1b[32m✓\x1b[0m",
117 "unregistered" => "\x1b[33m?\x1b[0m",
118 "unsealed" => "\x1b[33m○\x1b[0m",
119 _ => "\x1b[31m✗\x1b[0m",
120 };
121 let sha_short = if r.commit_sha.len() > 7 {
122 &r.commit_sha[..7]
123 } else {
124 &r.commit_sha
125 };
126 println!(" {} {} — {}", icon, sha_short, r.status);
127
128 if r.registered && r.sealed {
129 let sig_icon = if r.signature_valid {
130 "\x1b[32m✓\x1b[0m"
131 } else {
132 "\x1b[31m✗\x1b[0m"
133 };
134 let chain_icon = if r.chain_valid {
135 "\x1b[32m✓\x1b[0m"
136 } else {
137 "\x1b[31m✗\x1b[0m"
138 };
139 println!(" signature: {} chain: {}", sig_icon, chain_icon);
140 }
141
142 for p in &r.policy_results {
143 let p_icon = match p.result.as_str() {
144 "pass" => "\x1b[32m✓\x1b[0m",
145 "fail" if p.action == "block_push" => "\x1b[31m✗\x1b[0m",
146 "fail" => "\x1b[33m!\x1b[0m",
147 _ => " ",
148 };
149 println!(
150 " {} [{}] {} — {}",
151 p_icon, p.severity, p.rule_name, p.details
152 );
153 }
154 }
155
156 println!();
158 println!(
159 "Total: {} Registered: {} Sealed: {} Policy passed: {}",
160 result.total_commits,
161 result.registered_commits,
162 result.sealed_commits,
163 result.policy_passed_commits
164 );
165
166 if result.status == "pass" {
167 println!("\n\x1b[32mVerification passed.\x1b[0m");
168 Ok(())
169 } else {
170 eprintln!("\n\x1b[31mVerification failed.\x1b[0m");
171 std::process::exit(1);
172 }
173}