1use crate::config::DiffConfig;
6use crate::pipeline::{
7 auto_detect_format, compute_diff, exit_codes, output_report, parse_sbom_with_context,
8 OutputTarget,
9};
10use crate::reports::ReportFormat;
11use crate::tui::{run_tui, App};
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 enrichment_stats = {
41 if config.enrichment.enabled {
42 let osv_config = crate::pipeline::build_enrichment_config(&config.enrichment);
43 let stats_old =
44 crate::pipeline::enrich_sbom(old_parsed.sbom_mut(), &osv_config, quiet);
45 let stats_new =
46 crate::pipeline::enrich_sbom(new_parsed.sbom_mut(), &osv_config, quiet);
47 Some((stats_old, stats_new))
48 } else {
49 None
50 }
51 };
52
53 #[cfg(feature = "enrichment")]
55 {
56 if config.enrichment.enable_eol {
57 let eol_config = crate::enrichment::EolClientConfig {
58 cache_dir: config
59 .enrichment
60 .cache_dir
61 .clone()
62 .unwrap_or_else(crate::pipeline::dirs::eol_cache_dir),
63 cache_ttl: std::time::Duration::from_secs(config.enrichment.cache_ttl_hours * 3600),
64 bypass_cache: config.enrichment.bypass_cache,
65 timeout: std::time::Duration::from_secs(config.enrichment.timeout_secs),
66 ..Default::default()
67 };
68 crate::pipeline::enrich_eol(old_parsed.sbom_mut(), &eol_config, quiet);
69 crate::pipeline::enrich_eol(new_parsed.sbom_mut(), &eol_config, quiet);
70 }
71 }
72
73 #[cfg(feature = "enrichment")]
75 if !config.enrichment.vex_paths.is_empty() {
76 crate::pipeline::enrich_vex(old_parsed.sbom_mut(), &config.enrichment.vex_paths, quiet);
77 crate::pipeline::enrich_vex(new_parsed.sbom_mut(), &config.enrichment.vex_paths, quiet);
78 }
79
80 #[cfg(not(feature = "enrichment"))]
81 {
82 if config.enrichment.enabled && !quiet {
83 tracing::warn!(
84 "Enrichment requested but the 'enrichment' feature is not enabled. \
85 Rebuild with --features enrichment to enable vulnerability enrichment."
86 );
87 }
88 }
89
90 let result = compute_diff(&config, &old_parsed.sbom, &new_parsed.sbom)?;
92
93 let exit_code = determine_exit_code(&config, &result);
95
96 let output_target = OutputTarget::from_option(config.output.file.clone());
98 let effective_output = auto_detect_format(config.output.format, &output_target);
99
100 if effective_output == ReportFormat::Tui {
101 let (old_sbom, old_raw) = old_parsed.into_parts();
102 let (new_sbom, new_raw) = new_parsed.into_parts();
103
104 #[cfg(feature = "enrichment")]
105 let mut app = {
106 let app = App::new_diff(result, old_sbom, new_sbom, &old_raw, &new_raw);
107 if let Some((stats_old, stats_new)) = enrichment_stats {
108 app.with_enrichment_stats(stats_old, stats_new)
109 } else {
110 app
111 }
112 };
113
114 #[cfg(not(feature = "enrichment"))]
115 let mut app = App::new_diff(result, old_sbom, new_sbom, &old_raw, &new_raw);
116
117 run_tui(&mut app)?;
118 } else {
119 old_parsed.drop_raw_content();
120 new_parsed.drop_raw_content();
121 output_report(&config, &result, &old_parsed.sbom, &new_parsed.sbom)?;
122 }
123
124 Ok(exit_code)
125}
126
127const fn determine_exit_code(config: &DiffConfig, result: &crate::diff::DiffResult) -> i32 {
129 if config.behavior.fail_on_vuln && result.summary.vulnerabilities_introduced > 0 {
130 return exit_codes::VULNS_INTRODUCED;
131 }
132 if config.behavior.fail_on_change && result.summary.total_changes > 0 {
133 return exit_codes::CHANGES_DETECTED;
134 }
135 exit_codes::SUCCESS
136}
137
138#[cfg(test)]
139mod tests {
140 use crate::pipeline::OutputTarget;
141 use std::path::PathBuf;
142
143 #[test]
144 fn test_output_target_conversion() {
145 let none_target = OutputTarget::from_option(None);
146 assert!(matches!(none_target, OutputTarget::Stdout));
147
148 let some_target = OutputTarget::from_option(Some(PathBuf::from("/tmp/test.json")));
149 assert!(matches!(some_target, OutputTarget::File(_)));
150 }
151}