1use crate::config::DiffConfig;
6use crate::pipeline::{
7 OutputTarget, auto_detect_format, compute_diff, exit_codes, output_report,
8 parse_sbom_with_context,
9};
10use crate::reports::ReportFormat;
11use crate::tui::{App, run_tui};
12use anyhow::Result;
13
14#[allow(clippy::needless_pass_by_value)]
23pub fn run_diff(config: DiffConfig) -> Result<i32> {
24 let quiet = config.behavior.quiet;
25
26 let mut old_parsed = parse_sbom_with_context(&config.paths.old, quiet)?;
28 let mut new_parsed = parse_sbom_with_context(&config.paths.new, quiet)?;
29
30 if !quiet {
31 tracing::info!(
32 "Parsed {} components from old SBOM, {} from new SBOM",
33 old_parsed.sbom().component_count(),
34 new_parsed.sbom().component_count()
35 );
36 }
37
38 #[cfg(feature = "enrichment")]
40 let mut enrichment_warnings: Vec<&str> = Vec::new();
41
42 #[cfg(feature = "enrichment")]
43 let enrichment_stats = {
44 if config.enrichment.enabled {
45 let osv_config = crate::pipeline::build_enrichment_config(&config.enrichment);
46 let stats_old = crate::pipeline::enrich_sbom(old_parsed.sbom_mut(), &osv_config, quiet);
47 let stats_new = crate::pipeline::enrich_sbom(new_parsed.sbom_mut(), &osv_config, quiet);
48 if stats_old.is_none() || stats_new.is_none() {
49 enrichment_warnings.push("OSV vulnerability enrichment failed");
50 }
51 Some((stats_old, stats_new))
52 } else {
53 None
54 }
55 };
56
57 #[cfg(feature = "enrichment")]
59 {
60 if config.enrichment.enable_eol {
61 let eol_config = crate::enrichment::EolClientConfig {
62 cache_dir: config
63 .enrichment
64 .cache_dir
65 .clone()
66 .unwrap_or_else(crate::pipeline::dirs::eol_cache_dir),
67 cache_ttl: std::time::Duration::from_secs(config.enrichment.cache_ttl_hours * 3600),
68 bypass_cache: config.enrichment.bypass_cache,
69 timeout: std::time::Duration::from_secs(config.enrichment.timeout_secs),
70 ..Default::default()
71 };
72 let eol_old = crate::pipeline::enrich_eol(old_parsed.sbom_mut(), &eol_config, quiet);
73 let eol_new = crate::pipeline::enrich_eol(new_parsed.sbom_mut(), &eol_config, quiet);
74 if eol_old.is_none() || eol_new.is_none() {
75 enrichment_warnings.push("EOL enrichment failed");
76 }
77 }
78 }
79
80 #[cfg(feature = "enrichment")]
82 if !config.enrichment.vex_paths.is_empty() {
83 let vex_old =
84 crate::pipeline::enrich_vex(old_parsed.sbom_mut(), &config.enrichment.vex_paths, quiet);
85 let vex_new =
86 crate::pipeline::enrich_vex(new_parsed.sbom_mut(), &config.enrichment.vex_paths, quiet);
87 if vex_old.is_none() || vex_new.is_none() {
88 enrichment_warnings.push("VEX enrichment failed");
89 }
90 }
91
92 #[cfg(not(feature = "enrichment"))]
93 {
94 if config.enrichment.enabled {
95 eprintln!(
96 "Warning: enrichment requested but the 'enrichment' feature is not enabled. \
97 Rebuild with: cargo build --features enrichment"
98 );
99 }
100 }
101
102 let result = compute_diff(&config, &old_parsed.sbom, &new_parsed.sbom)?;
104
105 let exit_code = determine_exit_code(&config, &result);
107
108 let output_target = OutputTarget::from_option(config.output.file.clone());
110 let effective_output = auto_detect_format(config.output.format, &output_target);
111
112 if effective_output == ReportFormat::Tui {
113 let (old_sbom, old_raw) = old_parsed.into_parts();
114 let (new_sbom, new_raw) = new_parsed.into_parts();
115
116 #[cfg(feature = "enrichment")]
117 let mut app = {
118 let app = App::new_diff(result, old_sbom, new_sbom, &old_raw, &new_raw);
119 if let Some((stats_old, stats_new)) = enrichment_stats {
120 app.with_enrichment_stats(stats_old, stats_new)
121 } else {
122 app
123 }
124 };
125
126 #[cfg(not(feature = "enrichment"))]
127 let mut app = App::new_diff(result, old_sbom, new_sbom, &old_raw, &new_raw);
128
129 app.export_template = config.output.export_template.clone();
131
132 #[cfg(feature = "enrichment")]
134 if !enrichment_warnings.is_empty() {
135 app.set_status_message(format!("Warning: {}", enrichment_warnings.join(", ")));
136 app.status_sticky = true;
137 }
138
139 run_tui(&mut app)?;
140 } else {
141 old_parsed.drop_raw_content();
142 new_parsed.drop_raw_content();
143 output_report(&config, &result, &old_parsed.sbom, &new_parsed.sbom)?;
144 }
145
146 Ok(exit_code)
147}
148
149const fn determine_exit_code(config: &DiffConfig, result: &crate::diff::DiffResult) -> i32 {
151 if config.behavior.fail_on_vuln && result.summary.vulnerabilities_introduced > 0 {
152 return exit_codes::VULNS_INTRODUCED;
153 }
154 if config.behavior.fail_on_change && result.summary.total_changes > 0 {
155 return exit_codes::CHANGES_DETECTED;
156 }
157 exit_codes::SUCCESS
158}
159
160#[cfg(test)]
161mod tests {
162 use crate::pipeline::OutputTarget;
163 use std::path::PathBuf;
164
165 #[test]
166 fn test_output_target_conversion() {
167 let none_target = OutputTarget::from_option(None);
168 assert!(matches!(none_target, OutputTarget::Stdout));
169
170 let some_target = OutputTarget::from_option(Some(PathBuf::from("/tmp/test.json")));
171 assert!(matches!(some_target, OutputTarget::File(_)));
172 }
173}