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