1use crate::config::ViewConfig;
6use crate::model::{NormalizedSbom, Severity};
7use crate::pipeline::{
8 OutputTarget, auto_detect_format, parse_sbom_with_context, should_use_color, write_output,
9};
10use crate::reports::{ReportConfig, ReportFormat, create_reporter_with_options};
11use crate::tui::{ViewApp, run_view_tui};
12use anyhow::Result;
13
14#[allow(clippy::needless_pass_by_value)]
16pub fn run_view(config: ViewConfig) -> Result<i32> {
17 let mut parsed = parse_sbom_with_context(&config.sbom_path, false)?;
18
19 #[cfg(feature = "enrichment")]
21 let mut enrichment_warnings: Vec<&str> = Vec::new();
22
23 #[cfg(feature = "enrichment")]
24 if config.enrichment.enabled {
25 let osv_config = crate::pipeline::build_enrichment_config(&config.enrichment);
26 if crate::pipeline::enrich_sbom(parsed.sbom_mut(), &osv_config, false).is_none() {
27 enrichment_warnings.push("OSV vulnerability enrichment failed");
28 }
29 }
30
31 #[cfg(feature = "enrichment")]
33 if config.enrichment.enable_eol {
34 let eol_config = crate::enrichment::EolClientConfig {
35 cache_dir: config
36 .enrichment
37 .cache_dir
38 .clone()
39 .unwrap_or_else(crate::pipeline::dirs::eol_cache_dir),
40 cache_ttl: std::time::Duration::from_secs(config.enrichment.cache_ttl_hours * 3600),
41 bypass_cache: config.enrichment.bypass_cache,
42 timeout: std::time::Duration::from_secs(config.enrichment.timeout_secs),
43 ..Default::default()
44 };
45 if crate::pipeline::enrich_eol(parsed.sbom_mut(), &eol_config, false).is_none() {
46 enrichment_warnings.push("EOL enrichment failed");
47 }
48 }
49
50 #[cfg(feature = "enrichment")]
52 if !config.enrichment.vex_paths.is_empty()
53 && crate::pipeline::enrich_vex(parsed.sbom_mut(), &config.enrichment.vex_paths, false)
54 .is_none()
55 {
56 enrichment_warnings.push("VEX enrichment failed");
57 }
58
59 #[cfg(not(feature = "enrichment"))]
61 if config.enrichment.enabled || config.enrichment.enable_eol {
62 eprintln!(
63 "Warning: enrichment requested but the 'enrichment' feature is not enabled. \
64 Rebuild with: cargo build --features enrichment"
65 );
66 }
67
68 let filtered_count = apply_view_filters(parsed.sbom_mut(), &config);
70 if filtered_count > 0 {
71 tracing::info!(
72 "Filtered to {} components (removed {})",
73 parsed.sbom().component_count(),
74 filtered_count
75 );
76 }
77
78 if config.validate_ntia {
80 super::validate::validate_ntia_elements(parsed.sbom())?;
81 }
82
83 let output_target = OutputTarget::from_option(config.output.file.clone());
85 let effective_output = auto_detect_format(config.output.format, &output_target);
86
87 let vuln_count: usize = parsed
89 .sbom()
90 .components
91 .values()
92 .map(|c| c.vulnerabilities.len())
93 .sum();
94
95 if effective_output == ReportFormat::Tui {
96 let (sbom, raw_content) = parsed.into_parts();
97 let mut app = ViewApp::new(sbom, &raw_content);
98 app.export_template = config.output.export_template.clone();
99
100 #[cfg(feature = "enrichment")]
102 if !enrichment_warnings.is_empty() {
103 app.set_status_message(format!("Warning: {}", enrichment_warnings.join(", ")));
104 app.status_sticky = true;
105 }
106
107 run_view_tui(&mut app)?;
108 } else {
109 parsed.drop_raw_content();
110 output_view_report(&config, parsed.sbom(), &output_target)?;
111 }
112
113 if config.fail_on_vuln && vuln_count > 0 {
114 return Ok(crate::pipeline::exit_codes::VULNS_INTRODUCED);
115 }
116
117 Ok(crate::pipeline::exit_codes::SUCCESS)
118}
119
120pub fn apply_view_filters(sbom: &mut NormalizedSbom, config: &ViewConfig) -> usize {
122 let original_count = sbom.component_count();
123
124 let min_severity = config.min_severity.as_ref().map(|s| parse_severity(s));
126
127 let ecosystem_filter = config.ecosystem_filter.as_ref().map(|e| e.to_lowercase());
129
130 let keys_to_remove: Vec<_> = sbom
132 .components
133 .iter()
134 .filter_map(|(key, comp)| {
135 if config.vulnerable_only && comp.vulnerabilities.is_empty() {
137 return Some(key.clone());
138 }
139
140 if let Some(min_sev) = &min_severity {
142 let has_matching_vuln = comp.vulnerabilities.iter().any(|v| {
143 v.severity
144 .as_ref()
145 .is_some_and(|s| severity_meets_minimum(s, min_sev))
146 });
147 if !has_matching_vuln && !comp.vulnerabilities.is_empty() {
148 return Some(key.clone());
149 }
150 if config.vulnerable_only && !has_matching_vuln {
152 return Some(key.clone());
153 }
154 }
155
156 if let Some(eco_filter) = &ecosystem_filter {
158 let comp_eco = comp
159 .ecosystem
160 .as_ref()
161 .map(|e| format!("{e:?}").to_lowercase())
162 .unwrap_or_default();
163 if !comp_eco.contains(eco_filter) {
164 return Some(key.clone());
165 }
166 }
167
168 None
169 })
170 .collect();
171
172 for key in &keys_to_remove {
174 sbom.components.shift_remove(key);
175 }
176
177 original_count - sbom.component_count()
178}
179
180fn parse_severity(s: &str) -> Severity {
182 match s.to_lowercase().as_str() {
183 "critical" => Severity::Critical,
184 "high" => Severity::High,
185 "medium" => Severity::Medium,
186 "low" => Severity::Low,
187 _ => Severity::Unknown,
188 }
189}
190
191pub fn severity_meets_minimum(severity: &Severity, minimum: &Severity) -> bool {
193 let severity_order = |s: &Severity| match s {
194 Severity::Critical => 4,
195 Severity::High => 3,
196 Severity::Medium => 2,
197 Severity::Low => 1,
198 Severity::Info | Severity::None | Severity::Unknown => 0,
199 };
200
201 severity_order(severity) >= severity_order(minimum)
202}
203
204fn output_view_report(
206 config: &ViewConfig,
207 sbom: &NormalizedSbom,
208 output_target: &OutputTarget,
209) -> Result<()> {
210 let effective_output = auto_detect_format(config.output.format, output_target);
211
212 let cra_result =
214 crate::quality::ComplianceChecker::new(crate::quality::ComplianceLevel::CraPhase2)
215 .check(sbom);
216
217 let report_config = ReportConfig {
218 report_types: vec![config.output.report_types],
219 metadata: crate::reports::ReportMetadata {
220 old_sbom_path: Some(config.sbom_path.to_string_lossy().to_string()),
221 ..Default::default()
222 },
223 view_cra_compliance: Some(cra_result),
224 ..Default::default()
225 };
226
227 let use_color = should_use_color(config.output.no_color);
228 let reporter = create_reporter_with_options(effective_output, use_color);
229 let report = reporter.generate_view_report(sbom, &report_config)?;
230
231 write_output(&report, output_target, false)
232}
233
234#[cfg(test)]
235mod tests {
236 use super::*;
237
238 #[test]
239 fn test_parse_severity() {
240 assert!(matches!(parse_severity("critical"), Severity::Critical));
241 assert!(matches!(parse_severity("HIGH"), Severity::High));
242 assert!(matches!(parse_severity("Medium"), Severity::Medium));
243 assert!(matches!(parse_severity("low"), Severity::Low));
244 assert!(matches!(parse_severity("unknown"), Severity::Unknown));
245 assert!(matches!(parse_severity("invalid"), Severity::Unknown));
246 }
247
248 #[test]
249 fn test_severity_meets_minimum() {
250 assert!(severity_meets_minimum(&Severity::Critical, &Severity::High));
251 assert!(severity_meets_minimum(&Severity::High, &Severity::High));
252 assert!(!severity_meets_minimum(&Severity::Medium, &Severity::High));
253 assert!(!severity_meets_minimum(&Severity::Low, &Severity::High));
254 }
255
256 #[test]
257 fn test_severity_order() {
258 assert!(severity_meets_minimum(&Severity::Critical, &Severity::Low));
259 assert!(severity_meets_minimum(
260 &Severity::Critical,
261 &Severity::Medium
262 ));
263 assert!(severity_meets_minimum(&Severity::Critical, &Severity::High));
264 assert!(severity_meets_minimum(
265 &Severity::Critical,
266 &Severity::Critical
267 ));
268 }
269
270 #[test]
271 fn test_apply_view_filters_no_filters() {
272 let mut sbom = NormalizedSbom::default();
273 let config = ViewConfig {
274 sbom_path: std::path::PathBuf::from("test.json"),
275 output: crate::config::OutputConfig {
276 format: ReportFormat::Summary,
277 file: None,
278 report_types: crate::reports::ReportType::All,
279 no_color: false,
280 streaming: crate::config::StreamingConfig::default(),
281 export_template: None,
282 },
283 validate_ntia: false,
284 min_severity: None,
285 vulnerable_only: false,
286 ecosystem_filter: None,
287 fail_on_vuln: false,
288 enrichment: crate::config::EnrichmentConfig::default(),
289 };
290
291 let removed = apply_view_filters(&mut sbom, &config);
292 assert_eq!(removed, 0);
293 }
294}