Skip to main content

sbom_tools/cli/
verify.rs

1//! CLI handler for the `verify` command.
2//!
3//! Provides file hash verification and component hash auditing.
4
5use 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/// Verify action to perform
14#[derive(Debug, Clone, clap::Subcommand)]
15pub enum VerifyAction {
16    /// Verify file integrity against a hash value
17    Hash {
18        /// SBOM file to verify
19        file: PathBuf,
20        /// Expected hash (sha256:<hex>, sha512:<hex>, or bare hex)
21        #[arg(long)]
22        expected: Option<String>,
23        /// Read expected hash from a file (e.g., sbom.json.sha256)
24        #[arg(long, conflicts_with = "expected")]
25        hash_file: Option<PathBuf>,
26    },
27    /// Audit component hashes within an SBOM
28    AuditHashes {
29        /// SBOM file to audit
30        file: PathBuf,
31        /// Output format (table or json)
32        #[arg(short, long, default_value = "table")]
33        format: String,
34    },
35}
36
37/// Run the verify command.
38pub 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                // Try to find a sidecar hash file
51                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) // non-zero for CI gating
126            } else {
127                Ok(exit_codes::SUCCESS)
128            }
129        }
130    }
131}