#![deny(warnings)]
use std::collections::HashMap;
use crate::models::policy::{PolicyEvaluation, PolicyViolation, Severity};
#[derive(Debug, Clone, Default)]
pub struct ViolationsDashboard {
pub violations: Vec<PolicyViolation>,
pub workspace_scores: HashMap<String, f64>,
pub ranked_workspaces: Vec<(String, f64)>,
}
#[derive(Debug, Clone)]
pub struct ResolvedViolation {
pub violation: PolicyViolation,
pub resolved_at: String,
pub audit_note: String,
}
pub fn aggregate_violations(
per_workspace_evals: &[PolicyEvaluation],
all_violations: &[PolicyViolation],
) -> ViolationsDashboard {
let workspace_scores: HashMap<String, f64> = per_workspace_evals
.iter()
.map(|e| (e.workspace.clone(), e.compliance_score))
.collect();
let mut sorted_violations = all_violations.to_vec();
sorted_violations.sort_by(|a, b| {
let sev_cmp = a.rule.severity.cmp(&b.rule.severity);
if sev_cmp != std::cmp::Ordering::Equal {
return sev_cmp;
}
a.workspace.cmp(&b.workspace)
});
let ranked = rank_workspaces_by_compliance(&workspace_scores);
ViolationsDashboard {
violations: sorted_violations,
workspace_scores,
ranked_workspaces: ranked,
}
}
pub fn rank_workspaces_by_compliance(
workspace_scores: &HashMap<String, f64>,
) -> Vec<(String, f64)> {
let mut ranked: Vec<(String, f64)> = workspace_scores
.iter()
.map(|(name, score)| (name.clone(), *score))
.collect();
ranked.sort_by(|a, b| {
let a_score = (a.1 * 10.0).round() as i64;
let b_score = (b.1 * 10.0).round() as i64;
match a_score.cmp(&b_score) {
std::cmp::Ordering::Equal => a.0.cmp(&b.0),
other => other,
}
});
ranked
}
pub fn resolve_violations(
prev_active: &[PolicyViolation],
now_violated: &[PolicyViolation],
resolved_at: &str,
) -> Vec<ResolvedViolation> {
let still_violated: std::collections::HashSet<(String, String, Option<String>)> = now_violated
.iter()
.map(|v| (v.rule.id.clone(), v.workspace.clone(), v.asset_id.clone()))
.collect();
prev_active
.iter()
.filter(|v| {
!still_violated.contains(&(v.rule.id.clone(), v.workspace.clone(), v.asset_id.clone()))
})
.map(|v| ResolvedViolation {
violation: v.clone(),
resolved_at: resolved_at.to_string(),
audit_note: format!(
"violation_resolved: rule='{}' workspace='{}' asset={:?} resolved_at='{}'",
v.rule.id, v.workspace, v.asset_id, resolved_at
),
})
.collect()
}
#[allow(dead_code)]
fn severity_display_order() -> &'static [Severity] {
&[Severity::Critical, Severity::Warning, Severity::Info]
}
#[cfg(test)]
mod tests {
use super::*;
use crate::models::policy::{
PolicyRule, PolicyRuleType, PolicyScope, PolicyViolation, RuleResult, Severity,
};
fn make_eval(workspace: &str, passed: usize, total: usize) -> PolicyEvaluation {
let score = if total == 0 {
100.0
} else {
((passed as f64 / total as f64) * 10000.0).round() / 100.0
};
PolicyEvaluation {
workspace: workspace.to_string(),
results: (0..total)
.map(|i| RuleResult {
rule_id: format!("rule-{i}"),
passed: i < passed,
details: None,
})
.collect(),
compliance_score: score,
}
}
fn make_violation(
workspace: &str,
severity: Severity,
rule_id: &str,
asset_id: Option<&str>,
) -> PolicyViolation {
PolicyViolation {
rule: PolicyRule {
id: rule_id.to_string(),
rule_type: PolicyRuleType::MaxStale { threshold: 0 },
severity,
scope: PolicyScope::All,
description: String::new(),
},
workspace: workspace.to_string(),
asset_id: asset_id.map(|s| s.to_string()),
first_detected: "2026-01-01T00:00:00Z".to_string(),
details: String::new(),
remediation: String::new(),
}
}
#[test]
fn aggregate_empty() {
let dashboard = aggregate_violations(&[], &[]);
assert!(dashboard.violations.is_empty());
assert!(dashboard.workspace_scores.is_empty());
assert!(dashboard.ranked_workspaces.is_empty());
}
#[test]
fn aggregate_violations_sorted_by_severity_then_workspace() {
let evals = vec![make_eval("alpha", 0, 1), make_eval("beta", 1, 2)];
let violations = vec![
make_violation("beta", Severity::Warning, "w1", None),
make_violation("alpha", Severity::Critical, "c1", None),
make_violation("alpha", Severity::Warning, "w2", None),
make_violation("beta", Severity::Info, "i1", None),
];
let dash = aggregate_violations(&evals, &violations);
assert_eq!(dash.violations[0].rule.severity, Severity::Critical);
assert_eq!(dash.violations[0].workspace, "alpha");
assert_eq!(dash.violations[1].rule.severity, Severity::Warning);
assert_eq!(dash.violations[1].workspace, "alpha");
assert_eq!(dash.violations[2].rule.severity, Severity::Warning);
assert_eq!(dash.violations[2].workspace, "beta");
assert_eq!(dash.violations[3].rule.severity, Severity::Info);
}
#[test]
fn rank_workspaces_ascending() {
let mut scores = HashMap::new();
scores.insert("alpha".to_string(), 100.0);
scores.insert("beta".to_string(), 50.0);
scores.insert("gamma".to_string(), 75.0);
let ranked = rank_workspaces_by_compliance(&scores);
assert_eq!(ranked[0].0, "beta");
assert_eq!(ranked[1].0, "gamma");
assert_eq!(ranked[2].0, "alpha");
}
#[test]
fn rank_workspaces_tie_broken_by_name() {
let mut scores = HashMap::new();
scores.insert("z-ws".to_string(), 50.0);
scores.insert("a-ws".to_string(), 50.0);
let ranked = rank_workspaces_by_compliance(&scores);
assert_eq!(ranked[0].0, "a-ws");
assert_eq!(ranked[1].0, "z-ws");
}
#[test]
fn resolve_detects_remediated_violation() {
let v = make_violation("prod", Severity::Critical, "r1", Some("asset-a"));
let prev = vec![v.clone()];
let now: Vec<PolicyViolation> = vec![];
let resolved = resolve_violations(&prev, &now, "2026-06-13T12:00:00Z");
assert_eq!(resolved.len(), 1);
assert!(resolved[0].audit_note.contains("violation_resolved"));
assert!(resolved[0].audit_note.contains("r1"));
}
#[test]
fn resolve_skips_still_active_violations() {
let v = make_violation("prod", Severity::Critical, "r1", Some("asset-a"));
let prev = vec![v.clone()];
let now = vec![v.clone()];
let resolved = resolve_violations(&prev, &now, "2026-06-13T12:00:00Z");
assert!(resolved.is_empty());
}
#[test]
fn resolve_handles_empty_prev() {
let resolved = resolve_violations(&[], &[], "2026-06-13T12:00:00Z");
assert!(resolved.is_empty());
}
proptest::proptest! {
#![proptest_config(proptest::prelude::ProptestConfig::with_cases(256))]
#[test]
fn prop28a_violations_sorted_severity_then_workspace(
workspaces in proptest::collection::vec("[a-z][a-z0-9]{1,8}", 1..5),
severities in proptest::collection::vec(0usize..3, 1..8),
) {
let sev_arr = [Severity::Critical, Severity::Warning, Severity::Info];
let violations: Vec<PolicyViolation> = severities
.iter()
.enumerate()
.map(|(i, &sev_idx)| {
let ws = &workspaces[i % workspaces.len()];
make_violation(ws, sev_arr[sev_idx].clone(), &format!("r{i}"), None)
})
.collect();
let evals: Vec<PolicyEvaluation> = workspaces
.iter()
.map(|ws| make_eval(ws, 0, 1))
.collect();
let dash = aggregate_violations(&evals, &violations);
for w in dash.violations.windows(2) {
let a = &w[0];
let b = &w[1];
let sev_a = a.rule.severity.clone();
let sev_b = b.rule.severity.clone();
proptest::prop_assert!(
sev_a < sev_b || (sev_a == sev_b && a.workspace <= b.workspace),
"violations not sorted: ({:?},{}) > ({:?},{})",
sev_a, a.workspace, sev_b, b.workspace
);
}
}
#[test]
fn prop28b_ranking_ascending_by_score(
workspace_names in proptest::collection::vec("[a-z][a-z0-9]{1,8}", 1..6),
scores_raw in proptest::collection::vec(0u32..=100, 1..6),
) {
let n = workspace_names.len().min(scores_raw.len());
let mut score_map = HashMap::new();
for i in 0..n {
score_map.insert(workspace_names[i].clone(), scores_raw[i] as f64);
}
let ranked = rank_workspaces_by_compliance(&score_map);
for w in ranked.windows(2) {
let (name_a, score_a) = &w[0];
let (name_b, score_b) = &w[1];
let sa = (score_a * 10.0).round() as i64;
let sb = (score_b * 10.0).round() as i64;
proptest::prop_assert!(
sa < sb || (sa == sb && name_a <= name_b),
"ranking not ascending: ({},{}) > ({},{})",
name_a, score_a, name_b, score_b
);
}
proptest::prop_assert_eq!(ranked.len(), score_map.len());
}
}
}