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
14pub fn run_diff(config: DiffConfig) -> Result<i32> {
23 let quiet = config.behavior.quiet;
24
25 let mut old_parsed = parse_sbom_with_context(&config.paths.old, quiet)?;
27 let mut new_parsed = parse_sbom_with_context(&config.paths.new, quiet)?;
28
29 if !quiet {
30 tracing::info!(
31 "Parsed {} components from old SBOM, {} from new SBOM",
32 old_parsed.sbom().component_count(),
33 new_parsed.sbom().component_count()
34 );
35 }
36
37 #[cfg(feature = "enrichment")]
39 let enrichment_stats = {
40 if config.enrichment.enabled {
41 use crate::enrichment::OsvEnricherConfig;
42 use crate::pipeline::dirs;
43
44 let osv_config = OsvEnricherConfig {
45 cache_dir: config
46 .enrichment
47 .cache_dir
48 .clone()
49 .unwrap_or_else(dirs::osv_cache_dir),
50 cache_ttl: std::time::Duration::from_secs(config.enrichment.cache_ttl_hours * 3600),
51 bypass_cache: config.enrichment.bypass_cache,
52 timeout: std::time::Duration::from_secs(config.enrichment.timeout_secs),
53 ..Default::default()
54 };
55
56 let stats_old =
57 crate::pipeline::enrich_sbom(old_parsed.sbom_mut(), &osv_config, quiet);
58 let stats_new =
59 crate::pipeline::enrich_sbom(new_parsed.sbom_mut(), &osv_config, quiet);
60 Some((stats_old, stats_new))
61 } else {
62 None
63 }
64 };
65
66 #[cfg(not(feature = "enrichment"))]
67 {
68 if config.enrichment.enabled && !quiet {
69 tracing::warn!(
70 "Enrichment requested but the 'enrichment' feature is not enabled. \
71 Rebuild with --features enrichment to enable vulnerability enrichment."
72 );
73 }
74 }
75
76 let result = compute_diff(&config, &old_parsed.sbom, &new_parsed.sbom)?;
78
79 let exit_code = determine_exit_code(&config, &result);
81
82 let output_target = OutputTarget::from_option(config.output.file.clone());
84 let effective_output = auto_detect_format(config.output.format, &output_target);
85
86 if effective_output == ReportFormat::Tui {
87 let (old_sbom, old_raw) = old_parsed.into_parts();
88 let (new_sbom, new_raw) = new_parsed.into_parts();
89
90 #[cfg(feature = "enrichment")]
91 let mut app = {
92 let app = App::new_diff(result, old_sbom, new_sbom, old_raw, new_raw);
93 if let Some((stats_old, stats_new)) = enrichment_stats {
94 app.with_enrichment_stats(stats_old, stats_new)
95 } else {
96 app
97 }
98 };
99
100 #[cfg(not(feature = "enrichment"))]
101 let mut app = App::new_diff(result, old_sbom, new_sbom, old_raw, new_raw);
102
103 run_tui(&mut app)?;
104 } else {
105 old_parsed.drop_raw_content();
106 new_parsed.drop_raw_content();
107 output_report(&config, &result, &old_parsed.sbom, &new_parsed.sbom)?;
108 }
109
110 Ok(exit_code)
111}
112
113fn determine_exit_code(config: &DiffConfig, result: &crate::diff::DiffResult) -> i32 {
115 if config.behavior.fail_on_vuln && result.summary.vulnerabilities_introduced > 0 {
116 return exit_codes::VULNS_INTRODUCED;
117 }
118 if config.behavior.fail_on_change && result.summary.total_changes > 0 {
119 return exit_codes::CHANGES_DETECTED;
120 }
121 exit_codes::SUCCESS
122}
123
124#[cfg(test)]
125mod tests {
126 use crate::pipeline::OutputTarget;
127 use std::path::PathBuf;
128
129 #[test]
130 fn test_output_target_conversion() {
131 let none_target = OutputTarget::from_option(None);
132 assert!(matches!(none_target, OutputTarget::Stdout));
133
134 let some_target = OutputTarget::from_option(Some(PathBuf::from("/tmp/test.json")));
135 assert!(matches!(some_target, OutputTarget::File(_)));
136 }
137}