use super::app::App;
use super::app_states::{
ChangeType, ComponentFilter, DiffVulnItem, DiffVulnStatus, VulnFilter, sort_component_changes,
};
use crate::diff::SlaStatus;
fn matches_vuln_filter(vuln: &crate::diff::VulnerabilityDetail, filter: VulnFilter) -> bool {
match filter {
VulnFilter::Critical => vuln.severity == "Critical",
VulnFilter::High => vuln.severity == "High" || vuln.severity == "Critical",
VulnFilter::Kev => vuln.is_kev,
VulnFilter::Direct => vuln.component_depth == Some(1),
VulnFilter::Transitive => vuln.component_depth.is_some_and(|d| d > 1),
VulnFilter::VexActionable => vuln.is_vex_actionable(),
_ => true,
}
}
const fn vuln_category_includes(filter: VulnFilter) -> (bool, bool, bool) {
let introduced = matches!(
filter,
VulnFilter::All
| VulnFilter::Introduced
| VulnFilter::Critical
| VulnFilter::High
| VulnFilter::Kev
| VulnFilter::Direct
| VulnFilter::Transitive
| VulnFilter::VexActionable
);
let resolved = matches!(
filter,
VulnFilter::All
| VulnFilter::Resolved
| VulnFilter::Critical
| VulnFilter::High
| VulnFilter::Kev
| VulnFilter::Direct
| VulnFilter::Transitive
| VulnFilter::VexActionable
);
let persistent = matches!(
filter,
VulnFilter::All
| VulnFilter::Critical
| VulnFilter::High
| VulnFilter::Kev
| VulnFilter::Direct
| VulnFilter::Transitive
| VulnFilter::VexActionable
);
(introduced, resolved, persistent)
}
impl App {
pub(super) fn find_component_index_all(
&self,
name: &str,
change_type: Option<ChangeType>,
version: Option<&str>,
) -> Option<usize> {
let name_lower = name.to_lowercase();
let version_lower = version.map(str::to_lowercase);
self.diff_component_items(ComponentFilter::All)
.iter()
.position(|comp| {
let matches_type = change_type.is_none_or(|t| match t {
ChangeType::Added => comp.change_type == crate::diff::ChangeType::Added,
ChangeType::Removed => comp.change_type == crate::diff::ChangeType::Removed,
ChangeType::Modified => comp.change_type == crate::diff::ChangeType::Modified,
});
let matches_name = comp.name.to_lowercase() == name_lower;
let matches_version = version_lower.as_ref().is_none_or(|v| {
comp.new_version.as_deref().map(str::to_lowercase) == Some(v.clone())
|| comp.old_version.as_deref().map(str::to_lowercase) == Some(v.clone())
});
matches_type && matches_name && matches_version
})
}
#[must_use]
pub fn diff_component_items(
&self,
filter: ComponentFilter,
) -> Vec<&crate::diff::ComponentChange> {
let Some(diff) = self.data.diff_result.as_ref() else {
return Vec::new();
};
let mut items = Vec::new();
let effective = if filter.is_view_filter() && filter != ComponentFilter::All {
ComponentFilter::All
} else {
filter
};
if effective == ComponentFilter::All || effective == ComponentFilter::Added {
items.extend(diff.components.added.iter());
}
if effective == ComponentFilter::All || effective == ComponentFilter::Removed {
items.extend(diff.components.removed.iter());
}
if effective == ComponentFilter::All || effective == ComponentFilter::Modified {
items.extend(diff.components.modified.iter());
}
sort_component_changes(&mut items, self.components_state().sort_by);
items
}
#[must_use]
pub fn diff_component_count(&self, filter: ComponentFilter) -> usize {
let Some(diff) = self.data.diff_result.as_ref() else {
return 0;
};
match filter {
ComponentFilter::All | ComponentFilter::EolOnly | ComponentFilter::EolRisk => {
diff.components.added.len()
+ diff.components.removed.len()
+ diff.components.modified.len()
}
ComponentFilter::Added => diff.components.added.len(),
ComponentFilter::Removed => diff.components.removed.len(),
ComponentFilter::Modified => diff.components.modified.len(),
}
}
#[must_use]
pub fn diff_vulnerability_items(&self) -> Vec<DiffVulnItem<'_>> {
let Some(diff) = self.data.diff_result.as_ref() else {
return Vec::new();
};
let filter = self.vulnerabilities_state().filter;
let sort = &self.vulnerabilities_state().sort_by;
let mut all_vulns: Vec<DiffVulnItem<'_>> = Vec::new();
let (include_introduced, include_resolved, include_persistent) =
vuln_category_includes(filter);
if include_introduced {
for vuln in &diff.vulnerabilities.introduced {
if !matches_vuln_filter(vuln, filter) {
continue;
}
all_vulns.push(DiffVulnItem {
status: DiffVulnStatus::Introduced,
vuln,
});
}
}
if include_resolved {
for vuln in &diff.vulnerabilities.resolved {
if !matches_vuln_filter(vuln, filter) {
continue;
}
all_vulns.push(DiffVulnItem {
status: DiffVulnStatus::Resolved,
vuln,
});
}
}
if include_persistent {
for vuln in &diff.vulnerabilities.persistent {
if !matches_vuln_filter(vuln, filter) {
continue;
}
all_vulns.push(DiffVulnItem {
status: DiffVulnStatus::Persistent,
vuln,
});
}
}
let advanced = &self.vulnerabilities_state().advanced_filter;
if !advanced.is_empty() {
all_vulns.retain(|item| advanced.matches(item));
}
let reverse_graph = &self.dependencies_state().cached_reverse_graph;
match sort {
super::app_states::VulnSort::Severity => {
all_vulns.sort_by(|a, b| {
let sev_order = |s: &str| match s {
"Critical" => 0,
"High" => 1,
"Medium" => 2,
"Low" => 3,
_ => 4,
};
sev_order(&a.vuln.severity).cmp(&sev_order(&b.vuln.severity))
});
}
super::app_states::VulnSort::Id => {
all_vulns.sort_by(|a, b| a.vuln.id.cmp(&b.vuln.id));
}
super::app_states::VulnSort::Component => {
all_vulns.sort_by(|a, b| a.vuln.component_name.cmp(&b.vuln.component_name));
}
super::app_states::VulnSort::FixUrgency => {
all_vulns.sort_by(|a, b| {
let urgency_a = calculate_vuln_urgency(a.vuln, reverse_graph);
let urgency_b = calculate_vuln_urgency(b.vuln, reverse_graph);
urgency_b.cmp(&urgency_a) });
}
super::app_states::VulnSort::CvssScore => {
all_vulns.sort_by(|a, b| {
let score_a = a.vuln.cvss_score.unwrap_or(0.0);
let score_b = b.vuln.cvss_score.unwrap_or(0.0);
score_b
.partial_cmp(&score_a)
.unwrap_or(std::cmp::Ordering::Equal)
});
}
super::app_states::VulnSort::SlaUrgency => {
all_vulns.sort_by(|a, b| {
let sla_a = sla_sort_key(a.vuln);
let sla_b = sla_sort_key(b.vuln);
sla_a.cmp(&sla_b)
});
}
}
all_vulns
}
pub fn ensure_vulnerability_cache(&mut self) {
let current_key = (
self.vulnerabilities_state().filter,
self.vulnerabilities_state().sort_by,
);
if self.vulnerabilities_state().cached_key == Some(current_key)
&& !self.vulnerabilities_state().cached_indices.is_empty()
{
return; }
let items = self.diff_vulnerability_items();
let indices: Vec<(DiffVulnStatus, usize)> =
self.data
.diff_result
.as_ref()
.map_or_else(Vec::new, |diff| {
items
.iter()
.filter_map(|item| {
let list = match item.status {
DiffVulnStatus::Introduced => &diff.vulnerabilities.introduced,
DiffVulnStatus::Resolved => &diff.vulnerabilities.resolved,
DiffVulnStatus::Persistent => &diff.vulnerabilities.persistent,
};
let ptr = item.vuln as *const crate::diff::VulnerabilityDetail;
list.iter()
.position(|v| std::ptr::eq(v, ptr))
.map(|idx| (item.status, idx))
})
.collect()
});
drop(items);
self.vulnerabilities_state_mut().cached_key = Some(current_key);
self.vulnerabilities_state_mut().cached_indices = indices;
}
#[must_use]
pub fn diff_vulnerability_items_from_cache(&self) -> Vec<DiffVulnItem<'_>> {
let Some(diff) = self.data.diff_result.as_ref() else {
return Vec::new();
};
self.vulnerabilities_state()
.cached_indices
.iter()
.filter_map(|(status, idx)| {
let vuln = match status {
DiffVulnStatus::Introduced => diff.vulnerabilities.introduced.get(*idx),
DiffVulnStatus::Resolved => diff.vulnerabilities.resolved.get(*idx),
DiffVulnStatus::Persistent => diff.vulnerabilities.persistent.get(*idx),
}?;
Some(DiffVulnItem {
status: *status,
vuln,
})
})
.collect()
}
#[must_use]
pub fn diff_vulnerability_count(&self) -> usize {
if !self.vulnerabilities_state().advanced_filter.is_empty() {
return self.diff_vulnerability_items().len();
}
let Some(diff) = self.data.diff_result.as_ref() else {
return 0;
};
let filter = self.vulnerabilities_state().filter;
let (include_introduced, include_resolved, include_persistent) =
vuln_category_includes(filter);
let mut count = 0;
if include_introduced {
count += diff
.vulnerabilities
.introduced
.iter()
.filter(|v| matches_vuln_filter(v, filter))
.count();
}
if include_resolved {
count += diff
.vulnerabilities
.resolved
.iter()
.filter(|v| matches_vuln_filter(v, filter))
.count();
}
if include_persistent {
count += diff
.vulnerabilities
.persistent
.iter()
.filter(|v| matches_vuln_filter(v, filter))
.count();
}
count
}
pub(super) fn find_vulnerability_index(&self, id: &str) -> Option<usize> {
self.diff_vulnerability_items()
.iter()
.position(|item| item.vuln.id == id)
}
#[must_use]
pub fn get_new_sbom_sort_key(
&self,
id: &crate::model::CanonicalId,
) -> Option<&crate::model::ComponentSortKey> {
self.data
.new_sbom_index
.as_ref()
.and_then(|idx| idx.sort_key(id))
}
#[must_use]
pub fn get_old_sbom_sort_key(
&self,
id: &crate::model::CanonicalId,
) -> Option<&crate::model::ComponentSortKey> {
self.data
.old_sbom_index
.as_ref()
.and_then(|idx| idx.sort_key(id))
}
#[must_use]
pub fn get_sbom_sort_key(
&self,
id: &crate::model::CanonicalId,
) -> Option<&crate::model::ComponentSortKey> {
self.data
.sbom_index
.as_ref()
.and_then(|idx| idx.sort_key(id))
}
#[must_use]
pub fn get_dependencies_indexed(
&self,
id: &crate::model::CanonicalId,
) -> Vec<&crate::model::DependencyEdge> {
if let (Some(sbom), Some(idx)) = (&self.data.new_sbom, &self.data.new_sbom_index) {
idx.dependencies_of(id, &sbom.edges)
} else if let (Some(sbom), Some(idx)) = (&self.data.sbom, &self.data.sbom_index) {
idx.dependencies_of(id, &sbom.edges)
} else {
Vec::new()
}
}
#[must_use]
pub fn get_dependents_indexed(
&self,
id: &crate::model::CanonicalId,
) -> Vec<&crate::model::DependencyEdge> {
if let (Some(sbom), Some(idx)) = (&self.data.new_sbom, &self.data.new_sbom_index) {
idx.dependents_of(id, &sbom.edges)
} else if let (Some(sbom), Some(idx)) = (&self.data.sbom, &self.data.sbom_index) {
idx.dependents_of(id, &sbom.edges)
} else {
Vec::new()
}
}
}
fn calculate_vuln_urgency(
vuln: &crate::diff::VulnerabilityDetail,
reverse_graph: &std::collections::HashMap<String, Vec<String>>,
) -> u8 {
use crate::tui::security::{calculate_fix_urgency, severity_to_rank};
let severity_rank = severity_to_rank(&vuln.severity);
let cvss_score = vuln.cvss_score.unwrap_or(0.0);
let mut blast_radius = 0usize;
if let Some(direct_deps) = reverse_graph.get(&vuln.component_name) {
blast_radius = direct_deps.len();
for dep in direct_deps {
if let Some(transitive) = reverse_graph.get(dep) {
blast_radius += transitive.len();
}
}
}
calculate_fix_urgency(severity_rank, blast_radius, cvss_score)
}
fn sla_sort_key(vuln: &crate::diff::VulnerabilityDetail) -> i64 {
match vuln.sla_status() {
SlaStatus::Overdue(days) => -(days + crate::tui::constants::SLA_OVERDUE_SORT_OFFSET), SlaStatus::DueSoon(days) | SlaStatus::OnTrack(days) => days,
SlaStatus::NoDueDate => i64::MAX,
}
}