use crate::tui::state::ListNavigation;
pub struct VulnerabilitiesState {
pub selected: usize,
pub total: usize,
pub filter: VulnFilter,
pub sort_by: VulnSort,
pub group_by_component: bool,
pub expanded_groups: std::collections::HashSet<String>,
pub cached_key: Option<(VulnFilter, VulnSort)>,
pub cached_indices: Vec<(DiffVulnStatus, usize)>,
pub(crate) cached_attack_paths: Option<(String, Vec<crate::tui::security::AttackPath>)>,
pub(crate) grouped_cache_generation: u64,
pub advanced_filter: VulnFilterSpec,
}
impl VulnerabilitiesState {
pub fn new(total: usize) -> Self {
Self {
selected: 0,
total,
filter: VulnFilter::All,
sort_by: VulnSort::Severity,
group_by_component: false,
expanded_groups: std::collections::HashSet::new(),
cached_key: None,
cached_indices: Vec::new(),
cached_attack_paths: None,
grouped_cache_generation: 0,
advanced_filter: VulnFilterSpec::default(),
}
}
pub fn invalidate_cache(&mut self) {
self.cached_key = None;
self.cached_indices.clear();
self.invalidate_grouped_cache();
}
fn compute_grouped_hash(&self) -> u64 {
use std::hash::{Hash, Hasher};
let mut hasher = std::collections::hash_map::DefaultHasher::new();
self.filter.hash(&mut hasher);
self.sort_by.hash(&mut hasher);
self.group_by_component.hash(&mut hasher);
self.expanded_groups.len().hash(&mut hasher);
let mut sorted: Vec<&String> = self.expanded_groups.iter().collect();
sorted.sort();
for g in sorted {
g.hash(&mut hasher);
}
hasher.finish()
}
pub(crate) fn invalidate_grouped_cache(&mut self) {
self.grouped_cache_generation = self.compute_grouped_hash();
}
pub fn toggle_grouped_mode(&mut self) {
self.group_by_component = !self.group_by_component;
self.selected = 0;
self.invalidate_grouped_cache();
}
pub fn toggle_group(&mut self, component_id: &str) {
if self.expanded_groups.contains(component_id) {
self.expanded_groups.remove(component_id);
} else {
self.expanded_groups.insert(component_id.to_string());
}
self.invalidate_grouped_cache();
}
pub fn expand_all_groups(&mut self, group_ids: &[String]) {
for id in group_ids {
self.expanded_groups.insert(id.clone());
}
self.invalidate_grouped_cache();
}
pub fn collapse_all_groups(&mut self) {
self.expanded_groups.clear();
self.invalidate_grouped_cache();
}
pub fn is_group_expanded(&self, component_id: &str) -> bool {
self.expanded_groups.contains(component_id)
}
pub fn toggle_filter(&mut self) {
self.filter = self.filter.next();
self.selected = 0;
self.invalidate_cache();
}
pub fn toggle_sort(&mut self) {
self.sort_by = self.sort_by.next();
self.selected = 0;
self.invalidate_cache();
}
}
impl ListNavigation for VulnerabilitiesState {
fn selected(&self) -> usize {
self.selected
}
fn set_selected(&mut self, idx: usize) {
self.selected = idx;
}
fn total(&self) -> usize {
self.total
}
fn set_total(&mut self, total: usize) {
self.total = total;
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum VulnFilter {
All,
Introduced,
Resolved,
Critical,
High,
Kev,
Direct,
Transitive,
VexActionable,
}
impl VulnFilter {
pub const fn label(self) -> &'static str {
match self {
Self::All => "All",
Self::Introduced => "Introduced",
Self::Resolved => "Resolved",
Self::Critical => "Critical",
Self::High => "High",
Self::Kev => "KEV",
Self::Direct => "Direct",
Self::Transitive => "Transitive",
Self::VexActionable => "VEX Actionable",
}
}
pub const fn next(self) -> Self {
match self {
Self::All => Self::Introduced,
Self::Introduced => Self::Resolved,
Self::Resolved => Self::Critical,
Self::Critical => Self::High,
Self::High => Self::Kev,
Self::Kev => Self::Direct,
Self::Direct => Self::Transitive,
Self::Transitive => Self::VexActionable,
Self::VexActionable => Self::All,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum VulnSort {
#[default]
Severity,
Id,
Component,
FixUrgency,
CvssScore,
SlaUrgency,
}
impl VulnSort {
pub const fn next(self) -> Self {
match self {
Self::Severity => Self::FixUrgency,
Self::FixUrgency => Self::CvssScore,
Self::CvssScore => Self::SlaUrgency,
Self::SlaUrgency => Self::Component,
Self::Component => Self::Id,
Self::Id => Self::Severity,
}
}
pub const fn label(self) -> &'static str {
match self {
Self::Severity => "Severity",
Self::FixUrgency => "Fix Urgency",
Self::CvssScore => "CVSS Score",
Self::SlaUrgency => "SLA Urgency",
Self::Component => "Component",
Self::Id => "ID",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DiffVulnStatus {
Introduced,
Resolved,
Persistent,
}
impl DiffVulnStatus {
pub const fn label(self) -> &'static str {
match self {
Self::Introduced => "Introduced",
Self::Resolved => "Resolved",
Self::Persistent => "Persistent",
}
}
}
pub struct DiffVulnItem<'a> {
pub status: DiffVulnStatus,
pub vuln: &'a crate::diff::VulnerabilityDetail,
}
#[derive(Debug, Clone, Default)]
pub struct VulnFilterSpec {
pub severity: Option<String>,
pub status: Option<DiffVulnStatus>,
pub kev_only: bool,
pub actionable_only: bool,
}
impl VulnFilterSpec {
#[must_use]
pub fn is_empty(&self) -> bool {
self.severity.is_none() && self.status.is_none() && !self.kev_only && !self.actionable_only
}
#[must_use]
pub fn matches(&self, item: &DiffVulnItem<'_>) -> bool {
if let Some(ref sev) = self.severity
&& !item.vuln.severity.eq_ignore_ascii_case(sev)
{
return false;
}
if let Some(status) = self.status
&& item.status != status
{
return false;
}
if self.kev_only && !item.vuln.is_kev {
return false;
}
if self.actionable_only
&& let Some(ref vex) = item.vuln.vex_state
&& matches!(
vex,
crate::model::VexState::NotAffected | crate::model::VexState::Fixed
)
{
return false;
}
true
}
}