use crate::findings::types::{Confidence, Finding, Severity};
use crate::risk::RiskPriority;
use crate::scan::types::ScanSummary;
#[derive(Debug, Default, Clone, PartialEq, Eq)]
pub struct FindingFilter {
pub min_severity: Option<Severity>,
pub min_confidence: Option<Confidence>,
pub min_priority: Option<RiskPriority>,
pub rule_ids: Vec<String>,
}
impl FindingFilter {
pub fn is_empty(&self) -> bool {
self.min_severity.is_none()
&& self.min_confidence.is_none()
&& self.min_priority.is_none()
&& self.rule_ids.is_empty()
}
pub fn matches(&self, finding: &Finding) -> bool {
if let Some(min) = self.min_severity
&& !finding.severity.is_at_least(&min)
{
return false;
}
if let Some(min) = self.min_confidence
&& finding.confidence < min
{
return false;
}
if let Some(min) = self.min_priority
&& !finding.risk.priority.is_at_least(min)
{
return false;
}
if !self.rule_ids.is_empty()
&& !self
.rule_ids
.iter()
.any(|rule_id| rule_id == &finding.rule_id)
{
return false;
}
true
}
pub fn apply_to_summary(&self, summary: &mut ScanSummary) {
if !self.is_empty() {
summary.findings.retain(|finding| self.matches(finding));
}
recompute_summary_metrics(summary);
}
}
pub fn recompute_summary_metrics(summary: &mut ScanSummary) {
summary.visible_findings_count = summary.findings.len();
summary.health_score =
ScanSummary::compute_health_score(&summary.findings, summary.non_empty_lines);
}
#[cfg(test)]
mod tests {
use super::*;
use crate::risk::RiskAssessment;
use std::path::PathBuf;
fn finding(
rule_id: &str,
severity: Severity,
confidence: Confidence,
priority: RiskPriority,
) -> Finding {
Finding {
rule_id: rule_id.to_string(),
severity,
confidence,
risk: RiskAssessment {
priority,
..RiskAssessment::default()
},
..Finding::default()
}
}
#[test]
fn severity_threshold_matches_at_or_above_minimum() {
let filter = FindingFilter {
min_severity: Some(Severity::High),
..FindingFilter::default()
};
assert!(filter.matches(&finding(
"rule.high",
Severity::High,
Confidence::Medium,
RiskPriority::P3
)));
assert!(!filter.matches(&finding(
"rule.medium",
Severity::Medium,
Confidence::Medium,
RiskPriority::P3
)));
}
#[test]
fn confidence_threshold_matches_at_or_above_minimum() {
let filter = FindingFilter {
min_confidence: Some(Confidence::Medium),
..FindingFilter::default()
};
assert!(filter.matches(&finding(
"rule.medium",
Severity::Low,
Confidence::Medium,
RiskPriority::P3
)));
assert!(!filter.matches(&finding(
"rule.low",
Severity::Low,
Confidence::Low,
RiskPriority::P3
)));
}
#[test]
fn priority_threshold_matches_at_or_above_minimum() {
let filter = FindingFilter {
min_priority: Some(RiskPriority::P1),
..FindingFilter::default()
};
assert!(filter.matches(&finding(
"rule.p1",
Severity::Low,
Confidence::Medium,
RiskPriority::P1
)));
assert!(!filter.matches(&finding(
"rule.p2",
Severity::Low,
Confidence::Medium,
RiskPriority::P2
)));
}
#[test]
fn repeated_rule_filter_matches_any_rule_id() {
let filter = FindingFilter {
rule_ids: vec!["rule.one".to_string(), "rule.two".to_string()],
..FindingFilter::default()
};
assert!(filter.matches(&finding(
"rule.two",
Severity::Low,
Confidence::Medium,
RiskPriority::P3
)));
assert!(!filter.matches(&finding(
"rule.three",
Severity::Low,
Confidence::Medium,
RiskPriority::P3
)));
}
#[test]
fn combined_filter_requires_every_selected_threshold() {
let filter = FindingFilter {
min_severity: Some(Severity::Medium),
min_confidence: Some(Confidence::High),
min_priority: Some(RiskPriority::P2),
rule_ids: vec!["rule.keep".to_string()],
};
assert!(filter.matches(&finding(
"rule.keep",
Severity::High,
Confidence::High,
RiskPriority::P2
)));
assert!(!filter.matches(&finding(
"rule.keep",
Severity::High,
Confidence::Medium,
RiskPriority::P2
)));
assert!(!filter.matches(&finding(
"rule.drop",
Severity::High,
Confidence::High,
RiskPriority::P2
)));
}
#[test]
fn apply_to_summary_recomputes_visible_count_and_health_score() {
let filter = FindingFilter {
min_severity: Some(Severity::High),
..FindingFilter::default()
};
let mut summary = ScanSummary {
root_path: PathBuf::from("."),
non_empty_lines: 1_000,
findings: vec![
finding(
"rule.keep",
Severity::High,
Confidence::Medium,
RiskPriority::P3,
),
finding(
"rule.drop",
Severity::Low,
Confidence::Medium,
RiskPriority::P3,
),
],
..ScanSummary::default()
};
filter.apply_to_summary(&mut summary);
assert_eq!(summary.findings.len(), 1);
assert_eq!(summary.visible_findings_count, 1);
assert_eq!(summary.health_score, 95);
}
}