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(
33 short = 'f',
34 long = "output",
35 alias = "format",
36 default_value = "table"
37 )]
38 format: String,
39 },
40}
41
42pub 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 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) } else {
132 Ok(exit_codes::SUCCESS)
133 }
134 }
135 }
136}