Skip to main content

sbom_tools/tui/
app_impl_items.rs

1//! Item list building methods for App.
2
3use 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
10/// Check whether a vulnerability matches the active filter.
11fn 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
23/// Determine which vulnerability categories (introduced, resolved, persistent)
24/// should be included for a given filter.
25fn 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    /// Find component index in diff mode using the same ordering as the components view
63    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    /// Build diff-mode components list in the same order as the table.
92    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    /// Count diff-mode components matching the filter (without building full list).
117    /// More efficient than diff_component_items().len() for just getting a count.
118    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    /// Count view-mode components (without building full list).
137    pub fn view_component_count(&self) -> usize {
138        self.data.sbom.as_ref().map(|s| s.component_count()).unwrap_or(0)
139    }
140
141    /// Build view-mode components list in the same order as the table.
142    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    /// Build diff-mode vulnerabilities list in the same order as the table.
153    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        // Get blast radius data for FixUrgency sorting
202        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                // Sort by fix urgency (severity × blast radius)
225                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) // Higher urgency first
229                });
230            }
231            super::app_states::VulnSort::CvssScore => {
232                // Sort by CVSS score (highest first)
233                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                // Sort by SLA urgency (most overdue first)
241                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    /// Ensure the vulnerability cache is populated for the current filter+sort.
253    ///
254    /// Call this before `diff_vulnerability_items_from_cache()` to guarantee
255    /// the cache is warm.
256    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; // Cache is warm
263        }
264
265        // Cache miss: compute full list, extract stable indices, then drop items
266        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                    // Find the index by pointer identity
279                    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    /// Reconstruct vulnerability items from the cache (cheap pointer lookups).
295    ///
296    /// Panics if the cache has not been populated. Call `ensure_vulnerability_cache()`
297    /// first.
298    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    /// Count diff-mode vulnerabilities matching the current filter (without building full list).
322    /// More efficient than diff_vulnerability_items().len() for just getting a count.
323    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    /// Find a vulnerability index based on the current filter/sort settings
362    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    // ========================================================================
369    // Index access methods for O(1) lookups
370    // ========================================================================
371
372    /// Get the sort key for a component in the new SBOM (diff mode).
373    ///
374    /// Returns pre-computed lowercase strings to avoid repeated allocations during sorting.
375    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    /// Get the sort key for a component in the old SBOM (diff mode).
383    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    /// Get the sort key for a component in the single SBOM (view mode).
391    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    /// Get dependencies of a component using the cached index (O(k) instead of O(edges)).
399    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    /// Get dependents of a component using the cached index (O(k) instead of O(edges)).
413    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
427/// Calculate fix urgency for a vulnerability based on severity and blast radius
428fn 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    // Calculate blast radius for affected component
438    let mut blast_radius = 0usize;
439    if let Some(direct_deps) = reverse_graph.get(&vuln.component_name) {
440        blast_radius = direct_deps.len();
441        // Add transitive count (simplified - just use direct for performance)
442        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
452/// Calculate SLA sort key for a vulnerability (lower = more urgent)
453fn sla_sort_key(vuln: &crate::diff::VulnerabilityDetail) -> i64 {
454    match vuln.sla_status() {
455        SlaStatus::Overdue(days) => -(days + 10000), // Most urgent (very negative)
456        SlaStatus::DueSoon(days) => days,
457        SlaStatus::OnTrack(days) => days,
458        SlaStatus::NoDueDate => i64::MAX,
459    }
460}