1use std::path::PathBuf;
6
7use anyhow::Result;
8
9use crate::parsers::parse_sbom;
10use crate::pipeline::exit_codes;
11use crate::verification::{
12 ModelVerifyResult, audit_component_hashes, verify_file_hash, verify_model_dir,
13};
14
15#[derive(Debug, Clone, clap::Subcommand)]
17pub enum VerifyAction {
18 Hash {
20 file: PathBuf,
22 #[arg(long)]
24 expected: Option<String>,
25 #[arg(long, conflicts_with = "expected")]
27 hash_file: Option<PathBuf>,
28 },
29 AuditHashes {
31 file: PathBuf,
33 #[arg(
35 short = 'f',
36 long = "output",
37 alias = "format",
38 default_value = "table"
39 )]
40 format: String,
41 },
42 ModelWeights {
44 file: PathBuf,
46 #[arg(long = "model-dir")]
49 model_dir: PathBuf,
50 #[arg(
52 short = 'f',
53 long = "output",
54 alias = "format",
55 default_value = "table"
56 )]
57 format: String,
58 },
59}
60
61pub fn run_verify(action: VerifyAction, quiet: bool) -> Result<i32> {
63 match action {
64 VerifyAction::Hash {
65 file,
66 expected,
67 hash_file,
68 } => {
69 let expected_hash = if let Some(e) = expected {
70 e
71 } else if let Some(hf) = hash_file {
72 crate::verification::read_hash_file(&hf)?
73 } else {
74 let sha_path = file.with_extension(
76 file.extension()
77 .map(|e| format!("{}.sha256", e.to_string_lossy()))
78 .unwrap_or_else(|| "sha256".to_string()),
79 );
80 if sha_path.exists() {
81 if !quiet {
82 eprintln!("Using sidecar hash file: {}", sha_path.display());
83 }
84 crate::verification::read_hash_file(&sha_path)?
85 } else {
86 anyhow::bail!(
87 "no hash provided. Use --expected <hash> or --hash-file <path>, \
88 or place a .sha256 sidecar file alongside the SBOM"
89 );
90 }
91 };
92
93 let result = verify_file_hash(&file, &expected_hash)?;
94
95 if !quiet {
96 println!("{result}");
97 }
98
99 if result.verified {
100 Ok(exit_codes::SUCCESS)
101 } else {
102 Ok(exit_codes::CHANGES_DETECTED)
106 }
107 }
108 VerifyAction::AuditHashes { file, format } => {
109 let sbom = parse_sbom(&file)?;
110 let report = audit_component_hashes(&sbom);
111
112 if format == "json" {
113 println!("{}", serde_json::to_string_pretty(&report)?);
114 } else {
115 println!("Component Hash Audit");
116 println!("====================");
117 println!(
118 "Total: {} Strong: {} Weak-only: {} Missing: {}",
119 report.total_components,
120 report.strong_count,
121 report.weak_only_count,
122 report.missing_count
123 );
124 println!("Pass rate: {:.1}%\n", report.pass_rate());
125
126 if report.weak_only_count > 0 || report.missing_count > 0 {
127 println!("Issues:");
128 for comp in &report.components {
129 match comp.result {
130 crate::verification::HashAuditResult::WeakOnly => {
131 println!(
132 " WEAK {} {} ({})",
133 comp.name,
134 comp.version.as_deref().unwrap_or(""),
135 comp.algorithms.join(", ")
136 );
137 }
138 crate::verification::HashAuditResult::Missing => {
139 println!(
140 " MISSING {} {}",
141 comp.name,
142 comp.version.as_deref().unwrap_or("")
143 );
144 }
145 crate::verification::HashAuditResult::Strong => {}
146 }
147 }
148 }
149 }
150
151 if report.missing_count > 0 || report.weak_only_count > 0 {
152 Ok(exit_codes::CHANGES_DETECTED) } else {
154 Ok(exit_codes::SUCCESS)
155 }
156 }
157 VerifyAction::ModelWeights {
158 file,
159 model_dir,
160 format,
161 } => {
162 let sbom = parse_sbom(&file)?;
163 let report = verify_model_dir(&sbom, &model_dir);
164
165 if report.total_models == 0 {
168 anyhow::bail!("SBOM contains no model components; nothing to verify");
169 }
170
171 if format == "json" {
172 println!("{}", serde_json::to_string_pretty(&report)?);
173 } else {
174 println!("Model Weight Verification");
175 println!("=========================");
176 println!("Model dir: {}", report.model_dir);
177 println!(
178 "Models: {} Verified: {} Mismatch: {} Missing: {} No-hash: {}",
179 report.total_models,
180 report.verified_count,
181 report.mismatch_count,
182 report.missing_count,
183 report.no_hash_count,
184 );
185
186 for comp in &report.components {
187 match comp.result {
190 ModelVerifyResult::Verified => {
191 println!(
192 " {} {} {} -> {}",
193 comp.result.label(),
194 comp.name,
195 comp.version.as_deref().unwrap_or(""),
196 comp.file.as_deref().unwrap_or("?"),
197 );
198 }
199 _ => {
200 println!(
201 " {} {} {}{}",
202 comp.result.label(),
203 comp.name,
204 comp.version.as_deref().unwrap_or(""),
205 comp.hash
206 .as_deref()
207 .map(|h| format!(" ({h})"))
208 .unwrap_or_default(),
209 );
210 }
211 }
212 }
213 }
214
215 if report.has_failures() {
216 Ok(exit_codes::CHANGES_DETECTED)
218 } else {
219 Ok(exit_codes::SUCCESS)
220 }
221 }
222 }
223}