1use std::path::PathBuf;
6
7use anyhow::Result;
8
9use crate::parsers::parse_sbom;
10use crate::pipeline::exit_codes;
11use crate::verification::{audit_component_hashes, verify_file_hash};
12
13#[derive(Debug, Clone, clap::Subcommand)]
15pub enum VerifyAction {
16 Hash {
18 file: PathBuf,
20 #[arg(long)]
22 expected: Option<String>,
23 #[arg(long, conflicts_with = "expected")]
25 hash_file: Option<PathBuf>,
26 },
27 AuditHashes {
29 file: PathBuf,
31 #[arg(short, long, default_value = "table")]
33 format: String,
34 },
35}
36
37pub fn run_verify(action: VerifyAction, quiet: bool) -> Result<i32> {
39 match action {
40 VerifyAction::Hash {
41 file,
42 expected,
43 hash_file,
44 } => {
45 let expected_hash = if let Some(e) = expected {
46 e
47 } else if let Some(hf) = hash_file {
48 crate::verification::read_hash_file(&hf)?
49 } else {
50 let sha_path = file.with_extension(
52 file.extension()
53 .map(|e| format!("{}.sha256", e.to_string_lossy()))
54 .unwrap_or_else(|| "sha256".to_string()),
55 );
56 if sha_path.exists() {
57 if !quiet {
58 eprintln!("Using sidecar hash file: {}", sha_path.display());
59 }
60 crate::verification::read_hash_file(&sha_path)?
61 } else {
62 anyhow::bail!(
63 "no hash provided. Use --expected <hash> or --hash-file <path>, \
64 or place a .sha256 sidecar file alongside the SBOM"
65 );
66 }
67 };
68
69 let result = verify_file_hash(&file, &expected_hash)?;
70
71 if !quiet {
72 println!("{result}");
73 }
74
75 if result.verified {
76 Ok(exit_codes::SUCCESS)
77 } else {
78 Ok(exit_codes::ERROR)
79 }
80 }
81 VerifyAction::AuditHashes { file, format } => {
82 let sbom = parse_sbom(&file)?;
83 let report = audit_component_hashes(&sbom);
84
85 if format == "json" {
86 println!("{}", serde_json::to_string_pretty(&report)?);
87 } else {
88 println!("Component Hash Audit");
89 println!("====================");
90 println!(
91 "Total: {} Strong: {} Weak-only: {} Missing: {}",
92 report.total_components,
93 report.strong_count,
94 report.weak_only_count,
95 report.missing_count
96 );
97 println!("Pass rate: {:.1}%\n", report.pass_rate());
98
99 if report.weak_only_count > 0 || report.missing_count > 0 {
100 println!("Issues:");
101 for comp in &report.components {
102 match comp.result {
103 crate::verification::HashAuditResult::WeakOnly => {
104 println!(
105 " WEAK {} {} ({})",
106 comp.name,
107 comp.version.as_deref().unwrap_or(""),
108 comp.algorithms.join(", ")
109 );
110 }
111 crate::verification::HashAuditResult::Missing => {
112 println!(
113 " MISSING {} {}",
114 comp.name,
115 comp.version.as_deref().unwrap_or("")
116 );
117 }
118 crate::verification::HashAuditResult::Strong => {}
119 }
120 }
121 }
122 }
123
124 if report.missing_count > 0 || report.weak_only_count > 0 {
125 Ok(exit_codes::CHANGES_DETECTED) } else {
127 Ok(exit_codes::SUCCESS)
128 }
129 }
130 }
131}