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