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 });
79 let matches_name = comp.name.to_lowercase() == name_lower;
80 let matches_version = version_lower.as_ref().is_none_or(|v| {
81 comp.new_version.as_deref().map(str::to_lowercase) == Some(v.clone())
82 || comp.old_version.as_deref().map(str::to_lowercase) == Some(v.clone())
83 });
84
85 matches_type && matches_name && matches_version
86 })
87 }
88
89 #[must_use]
91 pub fn diff_component_items(
92 &self,
93 filter: ComponentFilter,
94 ) -> Vec<&crate::diff::ComponentChange> {
95 let Some(diff) = self.data.diff_result.as_ref() else {
96 return Vec::new();
97 };
98
99 let mut items = Vec::new();
100 let effective = if filter.is_view_filter() && filter != ComponentFilter::All {
102 ComponentFilter::All
103 } else {
104 filter
105 };
106 if effective == ComponentFilter::All || effective == ComponentFilter::Added {
107 items.extend(diff.components.added.iter());
108 }
109 if effective == ComponentFilter::All || effective == ComponentFilter::Removed {
110 items.extend(diff.components.removed.iter());
111 }
112 if effective == ComponentFilter::All || effective == ComponentFilter::Modified {
113 items.extend(diff.components.modified.iter());
114 }
115
116 sort_component_changes(&mut items, self.components_state().sort_by);
117 items
118 }
119
120 #[must_use]
123 pub fn diff_component_count(&self, filter: ComponentFilter) -> usize {
124 let Some(diff) = self.data.diff_result.as_ref() else {
125 return 0;
126 };
127
128 match filter {
129 ComponentFilter::All | ComponentFilter::EolOnly | ComponentFilter::EolRisk => {
130 diff.components.added.len()
131 + diff.components.removed.len()
132 + diff.components.modified.len()
133 }
134 ComponentFilter::Added => diff.components.added.len(),
135 ComponentFilter::Removed => diff.components.removed.len(),
136 ComponentFilter::Modified => diff.components.modified.len(),
137 }
138 }
139
140 #[must_use]
142 pub fn diff_vulnerability_items(&self) -> Vec<DiffVulnItem<'_>> {
143 let Some(diff) = self.data.diff_result.as_ref() else {
144 return Vec::new();
145 };
146 let filter = self.vulnerabilities_state().filter;
147 let sort = &self.vulnerabilities_state().sort_by;
148 let mut all_vulns: Vec<DiffVulnItem<'_>> = Vec::new();
149
150 let (include_introduced, include_resolved, include_persistent) =
151 vuln_category_includes(filter);
152
153 if include_introduced {
154 for vuln in &diff.vulnerabilities.introduced {
155 if !matches_vuln_filter(vuln, filter) {
156 continue;
157 }
158 all_vulns.push(DiffVulnItem {
159 status: DiffVulnStatus::Introduced,
160 vuln,
161 });
162 }
163 }
164
165 if include_resolved {
166 for vuln in &diff.vulnerabilities.resolved {
167 if !matches_vuln_filter(vuln, filter) {
168 continue;
169 }
170 all_vulns.push(DiffVulnItem {
171 status: DiffVulnStatus::Resolved,
172 vuln,
173 });
174 }
175 }
176
177 if include_persistent {
178 for vuln in &diff.vulnerabilities.persistent {
179 if !matches_vuln_filter(vuln, filter) {
180 continue;
181 }
182 all_vulns.push(DiffVulnItem {
183 status: DiffVulnStatus::Persistent,
184 vuln,
185 });
186 }
187 }
188
189 let advanced = &self.vulnerabilities_state().advanced_filter;
191 if !advanced.is_empty() {
192 all_vulns.retain(|item| advanced.matches(item));
193 }
194
195 let reverse_graph = &self.dependencies_state().cached_reverse_graph;
197
198 match sort {
199 super::app_states::VulnSort::Severity => {
200 all_vulns.sort_by(|a, b| {
201 let sev_order = |s: &str| match s {
202 "Critical" => 0,
203 "High" => 1,
204 "Medium" => 2,
205 "Low" => 3,
206 _ => 4,
207 };
208 sev_order(&a.vuln.severity).cmp(&sev_order(&b.vuln.severity))
209 });
210 }
211 super::app_states::VulnSort::Id => {
212 all_vulns.sort_by(|a, b| a.vuln.id.cmp(&b.vuln.id));
213 }
214 super::app_states::VulnSort::Component => {
215 all_vulns.sort_by(|a, b| a.vuln.component_name.cmp(&b.vuln.component_name));
216 }
217 super::app_states::VulnSort::FixUrgency => {
218 all_vulns.sort_by(|a, b| {
220 let urgency_a = calculate_vuln_urgency(a.vuln, reverse_graph);
221 let urgency_b = calculate_vuln_urgency(b.vuln, reverse_graph);
222 urgency_b.cmp(&urgency_a) });
224 }
225 super::app_states::VulnSort::CvssScore => {
226 all_vulns.sort_by(|a, b| {
228 let score_a = a.vuln.cvss_score.unwrap_or(0.0);
229 let score_b = b.vuln.cvss_score.unwrap_or(0.0);
230 score_b
231 .partial_cmp(&score_a)
232 .unwrap_or(std::cmp::Ordering::Equal)
233 });
234 }
235 super::app_states::VulnSort::SlaUrgency => {
236 all_vulns.sort_by(|a, b| {
238 let sla_a = sla_sort_key(a.vuln);
239 let sla_b = sla_sort_key(b.vuln);
240 sla_a.cmp(&sla_b)
241 });
242 }
243 }
244
245 all_vulns
246 }
247
248 pub fn ensure_vulnerability_cache(&mut self) {
253 let current_key = (
254 self.vulnerabilities_state().filter,
255 self.vulnerabilities_state().sort_by,
256 );
257
258 if self.vulnerabilities_state().cached_key == Some(current_key)
259 && !self.vulnerabilities_state().cached_indices.is_empty()
260 {
261 return; }
263
264 let items = self.diff_vulnerability_items();
266 let indices: Vec<(DiffVulnStatus, usize)> =
267 self.data
268 .diff_result
269 .as_ref()
270 .map_or_else(Vec::new, |diff| {
271 items
272 .iter()
273 .filter_map(|item| {
274 let list = match item.status {
275 DiffVulnStatus::Introduced => &diff.vulnerabilities.introduced,
276 DiffVulnStatus::Resolved => &diff.vulnerabilities.resolved,
277 DiffVulnStatus::Persistent => &diff.vulnerabilities.persistent,
278 };
279 let ptr = item.vuln as *const crate::diff::VulnerabilityDetail;
281 list.iter()
282 .position(|v| std::ptr::eq(v, ptr))
283 .map(|idx| (item.status, idx))
284 })
285 .collect()
286 });
287 drop(items);
288
289 self.vulnerabilities_state_mut().cached_key = Some(current_key);
290 self.vulnerabilities_state_mut().cached_indices = indices;
291 }
292
293 #[must_use]
298 pub fn diff_vulnerability_items_from_cache(&self) -> Vec<DiffVulnItem<'_>> {
299 let Some(diff) = self.data.diff_result.as_ref() else {
300 return Vec::new();
301 };
302 self.vulnerabilities_state()
303 .cached_indices
304 .iter()
305 .filter_map(|(status, idx)| {
306 let vuln = match status {
307 DiffVulnStatus::Introduced => diff.vulnerabilities.introduced.get(*idx),
308 DiffVulnStatus::Resolved => diff.vulnerabilities.resolved.get(*idx),
309 DiffVulnStatus::Persistent => diff.vulnerabilities.persistent.get(*idx),
310 }?;
311 Some(DiffVulnItem {
312 status: *status,
313 vuln,
314 })
315 })
316 .collect()
317 }
318
319 #[must_use]
325 pub fn diff_vulnerability_count(&self) -> usize {
326 if !self.vulnerabilities_state().advanced_filter.is_empty() {
329 return self.diff_vulnerability_items().len();
330 }
331
332 let Some(diff) = self.data.diff_result.as_ref() else {
333 return 0;
334 };
335 let filter = self.vulnerabilities_state().filter;
336
337 let (include_introduced, include_resolved, include_persistent) =
338 vuln_category_includes(filter);
339
340 let mut count = 0;
341 if include_introduced {
342 count += diff
343 .vulnerabilities
344 .introduced
345 .iter()
346 .filter(|v| matches_vuln_filter(v, filter))
347 .count();
348 }
349 if include_resolved {
350 count += diff
351 .vulnerabilities
352 .resolved
353 .iter()
354 .filter(|v| matches_vuln_filter(v, filter))
355 .count();
356 }
357 if include_persistent {
358 count += diff
359 .vulnerabilities
360 .persistent
361 .iter()
362 .filter(|v| matches_vuln_filter(v, filter))
363 .count();
364 }
365 count
366 }
367
368 pub(super) fn find_vulnerability_index(&self, id: &str) -> Option<usize> {
370 self.diff_vulnerability_items()
371 .iter()
372 .position(|item| item.vuln.id == id)
373 }
374
375 #[must_use]
383 pub fn get_new_sbom_sort_key(
384 &self,
385 id: &crate::model::CanonicalId,
386 ) -> Option<&crate::model::ComponentSortKey> {
387 self.data
388 .new_sbom_index
389 .as_ref()
390 .and_then(|idx| idx.sort_key(id))
391 }
392
393 #[must_use]
395 pub fn get_old_sbom_sort_key(
396 &self,
397 id: &crate::model::CanonicalId,
398 ) -> Option<&crate::model::ComponentSortKey> {
399 self.data
400 .old_sbom_index
401 .as_ref()
402 .and_then(|idx| idx.sort_key(id))
403 }
404
405 #[must_use]
407 pub fn get_sbom_sort_key(
408 &self,
409 id: &crate::model::CanonicalId,
410 ) -> Option<&crate::model::ComponentSortKey> {
411 self.data
412 .sbom_index
413 .as_ref()
414 .and_then(|idx| idx.sort_key(id))
415 }
416
417 #[must_use]
419 pub fn get_dependencies_indexed(
420 &self,
421 id: &crate::model::CanonicalId,
422 ) -> Vec<&crate::model::DependencyEdge> {
423 if let (Some(sbom), Some(idx)) = (&self.data.new_sbom, &self.data.new_sbom_index) {
424 idx.dependencies_of(id, &sbom.edges)
425 } else if let (Some(sbom), Some(idx)) = (&self.data.sbom, &self.data.sbom_index) {
426 idx.dependencies_of(id, &sbom.edges)
427 } else {
428 Vec::new()
429 }
430 }
431
432 #[must_use]
434 pub fn get_dependents_indexed(
435 &self,
436 id: &crate::model::CanonicalId,
437 ) -> Vec<&crate::model::DependencyEdge> {
438 if let (Some(sbom), Some(idx)) = (&self.data.new_sbom, &self.data.new_sbom_index) {
439 idx.dependents_of(id, &sbom.edges)
440 } else if let (Some(sbom), Some(idx)) = (&self.data.sbom, &self.data.sbom_index) {
441 idx.dependents_of(id, &sbom.edges)
442 } else {
443 Vec::new()
444 }
445 }
446}
447
448fn calculate_vuln_urgency(
450 vuln: &crate::diff::VulnerabilityDetail,
451 reverse_graph: &std::collections::HashMap<String, Vec<String>>,
452) -> u8 {
453 use crate::tui::security::{calculate_fix_urgency, severity_to_rank};
454
455 let severity_rank = severity_to_rank(&vuln.severity);
456 let cvss_score = vuln.cvss_score.unwrap_or(0.0);
457
458 let mut blast_radius = 0usize;
460 if let Some(direct_deps) = reverse_graph.get(&vuln.component_name) {
461 blast_radius = direct_deps.len();
462 for dep in direct_deps {
464 if let Some(transitive) = reverse_graph.get(dep) {
465 blast_radius += transitive.len();
466 }
467 }
468 }
469
470 calculate_fix_urgency(severity_rank, blast_radius, cvss_score)
471}
472
473fn sla_sort_key(vuln: &crate::diff::VulnerabilityDetail) -> i64 {
475 match vuln.sla_status() {
476 SlaStatus::Overdue(days) => -(days + crate::tui::constants::SLA_OVERDUE_SORT_OFFSET), SlaStatus::DueSoon(days) | SlaStatus::OnTrack(days) => days,
478 SlaStatus::NoDueDate => i64::MAX,
479 }
480}