1use crate::config::ViewConfig;
6use crate::model::{NormalizedSbom, Severity};
7use crate::pipeline::{
8 auto_detect_format, parse_sbom_with_context, should_use_color, write_output, OutputTarget,
9};
10use crate::reports::{create_reporter_with_options, ReportConfig, ReportFormat};
11use crate::tui::{run_view_tui, ViewApp};
12use anyhow::Result;
13
14pub fn run_view(config: ViewConfig) -> Result<()> {
16 let mut parsed = parse_sbom_with_context(&config.sbom_path, false)?;
17
18 let filtered_count = apply_view_filters(parsed.sbom_mut(), &config);
20 if filtered_count > 0 {
21 tracing::info!(
22 "Filtered to {} components (removed {})",
23 parsed.sbom().component_count(),
24 filtered_count
25 );
26 }
27
28 if config.validate_ntia {
30 super::validate::validate_ntia_elements(parsed.sbom())?;
31 }
32
33 let output_target = OutputTarget::from_option(config.output.file.clone());
35 let effective_output = auto_detect_format(config.output.format, &output_target);
36
37 if effective_output == ReportFormat::Tui {
38 let (sbom, raw_content) = parsed.into_parts();
39 let mut app = ViewApp::new(sbom, raw_content);
40 run_view_tui(&mut app)?;
41 } else {
42 parsed.drop_raw_content();
43 output_view_report(&config, parsed.sbom(), &output_target)?;
44 }
45
46 Ok(())
47}
48
49pub fn apply_view_filters(sbom: &mut NormalizedSbom, config: &ViewConfig) -> usize {
51 let original_count = sbom.component_count();
52
53 let min_severity = config
55 .min_severity
56 .as_ref()
57 .map(|s| parse_severity(s));
58
59 let ecosystem_filter = config.ecosystem_filter.as_ref().map(|e| e.to_lowercase());
61
62 let keys_to_remove: Vec<_> = sbom
64 .components
65 .iter()
66 .filter_map(|(key, comp)| {
67 if config.vulnerable_only && comp.vulnerabilities.is_empty() {
69 return Some(key.clone());
70 }
71
72 if let Some(min_sev) = &min_severity {
74 let has_matching_vuln = comp.vulnerabilities.iter().any(|v| {
75 v.severity
76 .as_ref()
77 .is_some_and(|s| severity_meets_minimum(s, min_sev))
78 });
79 if !has_matching_vuln && !comp.vulnerabilities.is_empty() {
80 return Some(key.clone());
81 }
82 if config.vulnerable_only && !has_matching_vuln {
84 return Some(key.clone());
85 }
86 }
87
88 if let Some(eco_filter) = &ecosystem_filter {
90 let comp_eco = comp
91 .ecosystem
92 .as_ref()
93 .map(|e| format!("{:?}", e).to_lowercase())
94 .unwrap_or_default();
95 if !comp_eco.contains(eco_filter) {
96 return Some(key.clone());
97 }
98 }
99
100 None
101 })
102 .collect();
103
104 for key in &keys_to_remove {
106 sbom.components.shift_remove(key);
107 }
108
109 original_count - sbom.component_count()
110}
111
112fn parse_severity(s: &str) -> Severity {
114 match s.to_lowercase().as_str() {
115 "critical" => Severity::Critical,
116 "high" => Severity::High,
117 "medium" => Severity::Medium,
118 "low" => Severity::Low,
119 _ => Severity::Unknown,
120 }
121}
122
123pub fn severity_meets_minimum(severity: &Severity, minimum: &Severity) -> bool {
125 let severity_order = |s: &Severity| match s {
126 Severity::Critical => 4,
127 Severity::High => 3,
128 Severity::Medium => 2,
129 Severity::Low => 1,
130 Severity::Info => 0,
131 Severity::None => 0,
132 Severity::Unknown => 0,
133 };
134
135 severity_order(severity) >= severity_order(minimum)
136}
137
138fn output_view_report(
140 config: &ViewConfig,
141 sbom: &NormalizedSbom,
142 output_target: &OutputTarget,
143) -> Result<()> {
144 let effective_output = auto_detect_format(config.output.format, output_target);
145
146 let cra_result = crate::quality::ComplianceChecker::new(crate::quality::ComplianceLevel::CraPhase2)
148 .check(sbom);
149
150 let report_config = ReportConfig {
151 metadata: crate::reports::ReportMetadata {
152 old_sbom_path: Some(config.sbom_path.to_string_lossy().to_string()),
153 ..Default::default()
154 },
155 view_cra_compliance: Some(cra_result),
156 ..Default::default()
157 };
158
159 let use_color = should_use_color(config.output.no_color);
160 let reporter = create_reporter_with_options(effective_output, use_color);
161 let report = reporter.generate_view_report(sbom, &report_config)?;
162
163 write_output(&report, output_target, false)
164}
165
166#[cfg(test)]
167mod tests {
168 use super::*;
169
170 #[test]
171 fn test_parse_severity() {
172 assert!(matches!(parse_severity("critical"), Severity::Critical));
173 assert!(matches!(parse_severity("HIGH"), Severity::High));
174 assert!(matches!(parse_severity("Medium"), Severity::Medium));
175 assert!(matches!(parse_severity("low"), Severity::Low));
176 assert!(matches!(parse_severity("unknown"), Severity::Unknown));
177 assert!(matches!(parse_severity("invalid"), Severity::Unknown));
178 }
179
180 #[test]
181 fn test_severity_meets_minimum() {
182 assert!(severity_meets_minimum(&Severity::Critical, &Severity::High));
183 assert!(severity_meets_minimum(&Severity::High, &Severity::High));
184 assert!(!severity_meets_minimum(&Severity::Medium, &Severity::High));
185 assert!(!severity_meets_minimum(&Severity::Low, &Severity::High));
186 }
187
188 #[test]
189 fn test_severity_order() {
190 assert!(severity_meets_minimum(&Severity::Critical, &Severity::Low));
191 assert!(severity_meets_minimum(&Severity::Critical, &Severity::Medium));
192 assert!(severity_meets_minimum(&Severity::Critical, &Severity::High));
193 assert!(severity_meets_minimum(&Severity::Critical, &Severity::Critical));
194 }
195
196 #[test]
197 fn test_apply_view_filters_no_filters() {
198 let mut sbom = NormalizedSbom::default();
199 let config = ViewConfig {
200 sbom_path: std::path::PathBuf::from("test.json"),
201 output: crate::config::OutputConfig {
202 format: ReportFormat::Summary,
203 file: None,
204 report_types: crate::reports::ReportType::All,
205 no_color: false,
206 streaming: crate::config::StreamingConfig::default(),
207 },
208 validate_ntia: false,
209 min_severity: None,
210 vulnerable_only: false,
211 ecosystem_filter: None,
212 };
213
214 let removed = apply_view_filters(&mut sbom, &config);
215 assert_eq!(removed, 0);
216 }
217}