1use super::app::App;
4use super::app_states::{
5 ChangeType, ComponentFilter, DiffVulnItem, DiffVulnStatus, VulnFilter, sort_component_changes,
6 sort_components,
7};
8use crate::diff::SlaStatus;
9
10fn eol_filter_matches(comp: &crate::model::Component, filter: ComponentFilter) -> bool {
12 use crate::model::EolStatus;
13 match filter {
14 ComponentFilter::EolOnly => comp
15 .eol
16 .as_ref()
17 .is_some_and(|e| e.status == EolStatus::EndOfLife),
18 ComponentFilter::EolRisk => comp.eol.as_ref().is_some_and(|e| {
19 matches!(
20 e.status,
21 EolStatus::EndOfLife | EolStatus::ApproachingEol | EolStatus::SecurityOnly
22 )
23 }),
24 _ => true,
25 }
26}
27
28fn matches_vuln_filter(vuln: &crate::diff::VulnerabilityDetail, filter: VulnFilter) -> bool {
30 match filter {
31 VulnFilter::Critical => vuln.severity == "Critical",
32 VulnFilter::High => vuln.severity == "High" || vuln.severity == "Critical",
33 VulnFilter::Kev => vuln.is_kev,
34 VulnFilter::Direct => vuln.component_depth == Some(1),
35 VulnFilter::Transitive => vuln.component_depth.is_some_and(|d| d > 1),
36 VulnFilter::VexActionable => vuln.is_vex_actionable(),
37 _ => true,
38 }
39}
40
41const fn vuln_category_includes(filter: VulnFilter) -> (bool, bool, bool) {
44 let introduced = matches!(
45 filter,
46 VulnFilter::All
47 | VulnFilter::Introduced
48 | VulnFilter::Critical
49 | VulnFilter::High
50 | VulnFilter::Kev
51 | VulnFilter::Direct
52 | VulnFilter::Transitive
53 | VulnFilter::VexActionable
54 );
55 let resolved = matches!(
56 filter,
57 VulnFilter::All
58 | VulnFilter::Resolved
59 | VulnFilter::Critical
60 | VulnFilter::High
61 | VulnFilter::Kev
62 | VulnFilter::Direct
63 | VulnFilter::Transitive
64 | VulnFilter::VexActionable
65 );
66 let persistent = matches!(
67 filter,
68 VulnFilter::All
69 | VulnFilter::Critical
70 | VulnFilter::High
71 | VulnFilter::Kev
72 | VulnFilter::Direct
73 | VulnFilter::Transitive
74 | VulnFilter::VexActionable
75 );
76 (introduced, resolved, persistent)
77}
78
79impl App {
80 pub(super) fn find_component_index_all(
82 &self,
83 name: &str,
84 change_type: Option<ChangeType>,
85 version: Option<&str>,
86 ) -> Option<usize> {
87 let name_lower = name.to_lowercase();
88 let version_lower = version.map(str::to_lowercase);
89
90 self.diff_component_items(ComponentFilter::All)
91 .iter()
92 .position(|comp| {
93 let matches_type = change_type.is_none_or(|t| match t {
94 ChangeType::Added => comp.change_type == crate::diff::ChangeType::Added,
95 ChangeType::Removed => comp.change_type == crate::diff::ChangeType::Removed,
96 ChangeType::Modified => comp.change_type == crate::diff::ChangeType::Modified,
97 });
98 let matches_name = comp.name.to_lowercase() == name_lower;
99 let matches_version = version_lower.as_ref().is_none_or(|v| {
100 comp.new_version.as_deref().map(str::to_lowercase) == Some(v.clone())
101 || comp.old_version.as_deref().map(str::to_lowercase) == Some(v.clone())
102 });
103
104 matches_type && matches_name && matches_version
105 })
106 }
107
108 #[must_use]
110 pub fn diff_component_items(
111 &self,
112 filter: ComponentFilter,
113 ) -> Vec<&crate::diff::ComponentChange> {
114 let Some(diff) = self.data.diff_result.as_ref() else {
115 return Vec::new();
116 };
117
118 let mut items = Vec::new();
119 let effective = if filter.is_view_filter() && filter != ComponentFilter::All {
121 ComponentFilter::All
122 } else {
123 filter
124 };
125 if effective == ComponentFilter::All || effective == ComponentFilter::Added {
126 items.extend(diff.components.added.iter());
127 }
128 if effective == ComponentFilter::All || effective == ComponentFilter::Removed {
129 items.extend(diff.components.removed.iter());
130 }
131 if effective == ComponentFilter::All || effective == ComponentFilter::Modified {
132 items.extend(diff.components.modified.iter());
133 }
134
135 sort_component_changes(&mut items, self.tabs.components.sort_by);
136 items
137 }
138
139 #[must_use]
142 pub fn diff_component_count(&self, filter: ComponentFilter) -> usize {
143 let Some(diff) = self.data.diff_result.as_ref() else {
144 return 0;
145 };
146
147 match filter {
148 ComponentFilter::All | ComponentFilter::EolOnly | ComponentFilter::EolRisk => {
149 diff.components.added.len()
150 + diff.components.removed.len()
151 + diff.components.modified.len()
152 }
153 ComponentFilter::Added => diff.components.added.len(),
154 ComponentFilter::Removed => diff.components.removed.len(),
155 ComponentFilter::Modified => diff.components.modified.len(),
156 }
157 }
158
159 pub fn view_component_count(&self) -> usize {
161 let Some(sbom) = self.data.sbom.as_ref() else {
162 return 0;
163 };
164 let filter = self.tabs.components.filter;
165 if filter == ComponentFilter::All || !filter.is_view_filter() {
166 sbom.component_count()
167 } else {
168 sbom.components
169 .values()
170 .filter(|c| eol_filter_matches(c, filter))
171 .count()
172 }
173 }
174
175 #[must_use]
177 pub fn view_component_items(&self) -> Vec<&crate::model::Component> {
178 let Some(sbom) = self.data.sbom.as_ref() else {
179 return Vec::new();
180 };
181 let filter = self.tabs.components.filter;
182 let mut items: Vec<_> = if filter == ComponentFilter::All || !filter.is_view_filter() {
183 sbom.components.values().collect()
184 } else {
185 sbom.components
186 .values()
187 .filter(|c| eol_filter_matches(c, filter))
188 .collect()
189 };
190 sort_components(&mut items, self.tabs.components.sort_by);
191 items
192 }
193
194 #[must_use]
196 pub fn diff_vulnerability_items(&self) -> Vec<DiffVulnItem<'_>> {
197 let Some(diff) = self.data.diff_result.as_ref() else {
198 return Vec::new();
199 };
200 let filter = self.tabs.vulnerabilities.filter;
201 let sort = &self.tabs.vulnerabilities.sort_by;
202 let mut all_vulns: Vec<DiffVulnItem<'_>> = Vec::new();
203
204 let (include_introduced, include_resolved, include_persistent) =
205 vuln_category_includes(filter);
206
207 if include_introduced {
208 for vuln in &diff.vulnerabilities.introduced {
209 if !matches_vuln_filter(vuln, filter) {
210 continue;
211 }
212 all_vulns.push(DiffVulnItem {
213 status: DiffVulnStatus::Introduced,
214 vuln,
215 });
216 }
217 }
218
219 if include_resolved {
220 for vuln in &diff.vulnerabilities.resolved {
221 if !matches_vuln_filter(vuln, filter) {
222 continue;
223 }
224 all_vulns.push(DiffVulnItem {
225 status: DiffVulnStatus::Resolved,
226 vuln,
227 });
228 }
229 }
230
231 if include_persistent {
232 for vuln in &diff.vulnerabilities.persistent {
233 if !matches_vuln_filter(vuln, filter) {
234 continue;
235 }
236 all_vulns.push(DiffVulnItem {
237 status: DiffVulnStatus::Persistent,
238 vuln,
239 });
240 }
241 }
242
243 let reverse_graph = &self.tabs.dependencies.cached_reverse_graph;
245
246 match sort {
247 super::app_states::VulnSort::Severity => {
248 all_vulns.sort_by(|a, b| {
249 let sev_order = |s: &str| match s {
250 "Critical" => 0,
251 "High" => 1,
252 "Medium" => 2,
253 "Low" => 3,
254 _ => 4,
255 };
256 sev_order(&a.vuln.severity).cmp(&sev_order(&b.vuln.severity))
257 });
258 }
259 super::app_states::VulnSort::Id => {
260 all_vulns.sort_by(|a, b| a.vuln.id.cmp(&b.vuln.id));
261 }
262 super::app_states::VulnSort::Component => {
263 all_vulns.sort_by(|a, b| a.vuln.component_name.cmp(&b.vuln.component_name));
264 }
265 super::app_states::VulnSort::FixUrgency => {
266 all_vulns.sort_by(|a, b| {
268 let urgency_a = calculate_vuln_urgency(a.vuln, reverse_graph);
269 let urgency_b = calculate_vuln_urgency(b.vuln, reverse_graph);
270 urgency_b.cmp(&urgency_a) });
272 }
273 super::app_states::VulnSort::CvssScore => {
274 all_vulns.sort_by(|a, b| {
276 let score_a = a.vuln.cvss_score.unwrap_or(0.0);
277 let score_b = b.vuln.cvss_score.unwrap_or(0.0);
278 score_b
279 .partial_cmp(&score_a)
280 .unwrap_or(std::cmp::Ordering::Equal)
281 });
282 }
283 super::app_states::VulnSort::SlaUrgency => {
284 all_vulns.sort_by(|a, b| {
286 let sla_a = sla_sort_key(a.vuln);
287 let sla_b = sla_sort_key(b.vuln);
288 sla_a.cmp(&sla_b)
289 });
290 }
291 }
292
293 all_vulns
294 }
295
296 pub fn ensure_vulnerability_cache(&mut self) {
301 let current_key = (
302 self.tabs.vulnerabilities.filter,
303 self.tabs.vulnerabilities.sort_by,
304 );
305
306 if self.tabs.vulnerabilities.cached_key == Some(current_key)
307 && !self.tabs.vulnerabilities.cached_indices.is_empty()
308 {
309 return; }
311
312 let items = self.diff_vulnerability_items();
314 let indices: Vec<(DiffVulnStatus, usize)> =
315 self.data
316 .diff_result
317 .as_ref()
318 .map_or_else(Vec::new, |diff| {
319 items
320 .iter()
321 .filter_map(|item| {
322 let list = match item.status {
323 DiffVulnStatus::Introduced => &diff.vulnerabilities.introduced,
324 DiffVulnStatus::Resolved => &diff.vulnerabilities.resolved,
325 DiffVulnStatus::Persistent => &diff.vulnerabilities.persistent,
326 };
327 let ptr = item.vuln as *const crate::diff::VulnerabilityDetail;
329 list.iter()
330 .position(|v| std::ptr::eq(v, ptr))
331 .map(|idx| (item.status, idx))
332 })
333 .collect()
334 });
335 drop(items);
336
337 self.tabs.vulnerabilities.cached_key = Some(current_key);
338 self.tabs.vulnerabilities.cached_indices = indices;
339 }
340
341 #[must_use]
346 pub fn diff_vulnerability_items_from_cache(&self) -> Vec<DiffVulnItem<'_>> {
347 let Some(diff) = self.data.diff_result.as_ref() else {
348 return Vec::new();
349 };
350 self.tabs
351 .vulnerabilities
352 .cached_indices
353 .iter()
354 .filter_map(|(status, idx)| {
355 let vuln = match status {
356 DiffVulnStatus::Introduced => diff.vulnerabilities.introduced.get(*idx),
357 DiffVulnStatus::Resolved => diff.vulnerabilities.resolved.get(*idx),
358 DiffVulnStatus::Persistent => diff.vulnerabilities.persistent.get(*idx),
359 }?;
360 Some(DiffVulnItem {
361 status: *status,
362 vuln,
363 })
364 })
365 .collect()
366 }
367
368 #[must_use]
371 pub fn diff_vulnerability_count(&self) -> usize {
372 let Some(diff) = self.data.diff_result.as_ref() else {
373 return 0;
374 };
375 let filter = self.tabs.vulnerabilities.filter;
376
377 let (include_introduced, include_resolved, include_persistent) =
378 vuln_category_includes(filter);
379
380 let mut count = 0;
381 if include_introduced {
382 count += diff
383 .vulnerabilities
384 .introduced
385 .iter()
386 .filter(|v| matches_vuln_filter(v, filter))
387 .count();
388 }
389 if include_resolved {
390 count += diff
391 .vulnerabilities
392 .resolved
393 .iter()
394 .filter(|v| matches_vuln_filter(v, filter))
395 .count();
396 }
397 if include_persistent {
398 count += diff
399 .vulnerabilities
400 .persistent
401 .iter()
402 .filter(|v| matches_vuln_filter(v, filter))
403 .count();
404 }
405 count
406 }
407
408 pub(super) fn find_vulnerability_index(&self, id: &str) -> Option<usize> {
410 self.diff_vulnerability_items()
411 .iter()
412 .position(|item| item.vuln.id == id)
413 }
414
415 #[must_use]
423 pub fn get_new_sbom_sort_key(
424 &self,
425 id: &crate::model::CanonicalId,
426 ) -> Option<&crate::model::ComponentSortKey> {
427 self.data
428 .new_sbom_index
429 .as_ref()
430 .and_then(|idx| idx.sort_key(id))
431 }
432
433 #[must_use]
435 pub fn get_old_sbom_sort_key(
436 &self,
437 id: &crate::model::CanonicalId,
438 ) -> Option<&crate::model::ComponentSortKey> {
439 self.data
440 .old_sbom_index
441 .as_ref()
442 .and_then(|idx| idx.sort_key(id))
443 }
444
445 #[must_use]
447 pub fn get_sbom_sort_key(
448 &self,
449 id: &crate::model::CanonicalId,
450 ) -> Option<&crate::model::ComponentSortKey> {
451 self.data
452 .sbom_index
453 .as_ref()
454 .and_then(|idx| idx.sort_key(id))
455 }
456
457 #[must_use]
459 pub fn get_dependencies_indexed(
460 &self,
461 id: &crate::model::CanonicalId,
462 ) -> Vec<&crate::model::DependencyEdge> {
463 if let (Some(sbom), Some(idx)) = (&self.data.new_sbom, &self.data.new_sbom_index) {
464 idx.dependencies_of(id, &sbom.edges)
465 } else if let (Some(sbom), Some(idx)) = (&self.data.sbom, &self.data.sbom_index) {
466 idx.dependencies_of(id, &sbom.edges)
467 } else {
468 Vec::new()
469 }
470 }
471
472 #[must_use]
474 pub fn get_dependents_indexed(
475 &self,
476 id: &crate::model::CanonicalId,
477 ) -> Vec<&crate::model::DependencyEdge> {
478 if let (Some(sbom), Some(idx)) = (&self.data.new_sbom, &self.data.new_sbom_index) {
479 idx.dependents_of(id, &sbom.edges)
480 } else if let (Some(sbom), Some(idx)) = (&self.data.sbom, &self.data.sbom_index) {
481 idx.dependents_of(id, &sbom.edges)
482 } else {
483 Vec::new()
484 }
485 }
486}
487
488fn calculate_vuln_urgency(
490 vuln: &crate::diff::VulnerabilityDetail,
491 reverse_graph: &std::collections::HashMap<String, Vec<String>>,
492) -> u8 {
493 use crate::tui::security::{calculate_fix_urgency, severity_to_rank};
494
495 let severity_rank = severity_to_rank(&vuln.severity);
496 let cvss_score = vuln.cvss_score.unwrap_or(0.0);
497
498 let mut blast_radius = 0usize;
500 if let Some(direct_deps) = reverse_graph.get(&vuln.component_name) {
501 blast_radius = direct_deps.len();
502 for dep in direct_deps {
504 if let Some(transitive) = reverse_graph.get(dep) {
505 blast_radius += transitive.len();
506 }
507 }
508 }
509
510 calculate_fix_urgency(severity_rank, blast_radius, cvss_score)
511}
512
513fn sla_sort_key(vuln: &crate::diff::VulnerabilityDetail) -> i64 {
515 match vuln.sla_status() {
516 SlaStatus::Overdue(days) => -(days + crate::tui::constants::SLA_OVERDUE_SORT_OFFSET), SlaStatus::DueSoon(days) | SlaStatus::OnTrack(days) => days,
518 SlaStatus::NoDueDate => i64::MAX,
519 }
520}