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(
33            short = 'f',
34            long = "output",
35            alias = "format",
36            default_value = "table"
37        )]
38        format: String,
39    },
40}
41
42/// Run the verify command.
43pub fn run_verify(action: VerifyAction, quiet: bool) -> Result<i32> {
44    match action {
45        VerifyAction::Hash {
46            file,
47            expected,
48            hash_file,
49        } => {
50            let expected_hash = if let Some(e) = expected {
51                e
52            } else if let Some(hf) = hash_file {
53                crate::verification::read_hash_file(&hf)?
54            } else {
55                // Try to find a sidecar hash file
56                let sha_path = file.with_extension(
57                    file.extension()
58                        .map(|e| format!("{}.sha256", e.to_string_lossy()))
59                        .unwrap_or_else(|| "sha256".to_string()),
60                );
61                if sha_path.exists() {
62                    if !quiet {
63                        eprintln!("Using sidecar hash file: {}", sha_path.display());
64                    }
65                    crate::verification::read_hash_file(&sha_path)?
66                } else {
67                    anyhow::bail!(
68                        "no hash provided. Use --expected <hash> or --hash-file <path>, \
69                         or place a .sha256 sidecar file alongside the SBOM"
70                    );
71                }
72            };
73
74            let result = verify_file_hash(&file, &expected_hash)?;
75
76            if !quiet {
77                println!("{result}");
78            }
79
80            if result.verified {
81                Ok(exit_codes::SUCCESS)
82            } else {
83                Ok(exit_codes::ERROR)
84            }
85        }
86        VerifyAction::AuditHashes { file, format } => {
87            let sbom = parse_sbom(&file)?;
88            let report = audit_component_hashes(&sbom);
89
90            if format == "json" {
91                println!("{}", serde_json::to_string_pretty(&report)?);
92            } else {
93                println!("Component Hash Audit");
94                println!("====================");
95                println!(
96                    "Total: {}  Strong: {}  Weak-only: {}  Missing: {}",
97                    report.total_components,
98                    report.strong_count,
99                    report.weak_only_count,
100                    report.missing_count
101                );
102                println!("Pass rate: {:.1}%\n", report.pass_rate());
103
104                if report.weak_only_count > 0 || report.missing_count > 0 {
105                    println!("Issues:");
106                    for comp in &report.components {
107                        match comp.result {
108                            crate::verification::HashAuditResult::WeakOnly => {
109                                println!(
110                                    "  WEAK   {} {} ({})",
111                                    comp.name,
112                                    comp.version.as_deref().unwrap_or(""),
113                                    comp.algorithms.join(", ")
114                                );
115                            }
116                            crate::verification::HashAuditResult::Missing => {
117                                println!(
118                                    "  MISSING {} {}",
119                                    comp.name,
120                                    comp.version.as_deref().unwrap_or("")
121                                );
122                            }
123                            crate::verification::HashAuditResult::Strong => {}
124                        }
125                    }
126                }
127            }
128
129            if report.missing_count > 0 || report.weak_only_count > 0 {
130                Ok(exit_codes::CHANGES_DETECTED) // non-zero for CI gating
131            } else {
132                Ok(exit_codes::SUCCESS)
133            }
134        }
135    }
136}