use crate::{AttackResult, PropertyScore};
const PROPERTY_WEIGHTS: [(&str, f64); 10] = [
("P1", 0.15), ("P2", 0.12), ("P3", 0.05), ("P4", 0.15), ("P5", 0.10), ("P6", 0.12), ("P7", 0.10), ("P8", 0.08), ("P9", 0.08), ("P10", 0.05), ];
const PROPERTY_NAMES: [(&str, &str); 10] = [
("P1", "Tool-Level Access Control"),
("P2", "Parameter Constraint Enforcement"),
("P3", "Priority Monotonicity"),
("P4", "Injection Resistance"),
("P5", "Schema Integrity"),
("P6", "Response Confidentiality"),
("P7", "Audit Immutability"),
("P8", "Delegation Monotonicity"),
("P9", "Unicode Normalization"),
("P10", "Temporal Consistency"),
];
fn attack_to_properties(attack_id: &str) -> Vec<&'static str> {
let prefix = attack_id.split('.').next().unwrap_or(attack_id);
match prefix {
"A1" => vec!["P4", "P9"],
"A2" => vec!["P5"],
"A3" => vec!["P1", "P2"],
"A4" => vec!["P6"],
"A5" => {
match attack_id {
"A5.1" => vec!["P1"],
"A5.2" | "A5.3" => vec!["P3"],
"A5.4" => vec!["P8"],
"A5.5" => vec!["P1", "P2"],
_ => vec!["P1", "P3", "P8"],
}
}
"A6" => vec!["P4", "P6"],
"A7" => vec!["P5", "P9"],
"A8" => vec!["P7"],
"A9" => vec!["P2"],
"A10" => vec!["P10"],
"A11" => vec!["P2", "P6"],
"A12" => vec!["P1", "P4"],
_ => vec![],
}
}
pub fn calculate_property_scores(attacks: &[AttackResult]) -> Vec<PropertyScore> {
PROPERTY_NAMES
.iter()
.map(|(pid, name)| {
let mut passed = 0usize;
let mut total = 0usize;
for attack in attacks {
let props = attack_to_properties(&attack.attack_id);
if props.contains(pid) {
total += 1;
if attack.passed {
passed += 1;
}
}
}
let score = if total > 0 {
(passed as f64 / total as f64) * 100.0
} else {
0.0
};
PropertyScore {
property_id: pid.to_string(),
name: name.to_string(),
score,
tests_passed: passed,
tests_total: total,
}
})
.collect()
}
pub fn calculate_overall_score(properties: &[PropertyScore]) -> f64 {
let mut weighted_sum = 0.0;
for (pid, weight) in &PROPERTY_WEIGHTS {
if let Some(prop) = properties.iter().find(|p| p.property_id == *pid) {
weighted_sum += prop.score * weight;
}
}
weighted_sum
}
pub fn score_to_tier(score: f64) -> u8 {
match score as u32 {
0..=19 => 0,
20..=39 => 1,
40..=59 => 2,
60..=79 => 3,
80..=94 => 4,
_ => 5,
}
}
pub fn tier_name(tier: u8) -> &'static str {
match tier {
0 => "Unsafe",
1 => "Basic",
2 => "Moderate",
3 => "Strong",
4 => "Comprehensive",
_ => "Hardened",
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_score_to_tier() {
assert_eq!(score_to_tier(0.0), 0);
assert_eq!(score_to_tier(19.0), 0);
assert_eq!(score_to_tier(20.0), 1);
assert_eq!(score_to_tier(39.0), 1);
assert_eq!(score_to_tier(40.0), 2);
assert_eq!(score_to_tier(59.0), 2);
assert_eq!(score_to_tier(60.0), 3);
assert_eq!(score_to_tier(79.0), 3);
assert_eq!(score_to_tier(80.0), 4);
assert_eq!(score_to_tier(94.0), 4);
assert_eq!(score_to_tier(95.0), 5);
assert_eq!(score_to_tier(100.0), 5);
}
#[test]
fn test_tier_name() {
assert_eq!(tier_name(0), "Unsafe");
assert_eq!(tier_name(1), "Basic");
assert_eq!(tier_name(2), "Moderate");
assert_eq!(tier_name(3), "Strong");
assert_eq!(tier_name(4), "Comprehensive");
assert_eq!(tier_name(5), "Hardened");
}
#[test]
fn test_weights_sum_to_one() {
let sum: f64 = PROPERTY_WEIGHTS.iter().map(|(_, w)| w).sum();
assert!(
(sum - 1.0).abs() < 0.001,
"Property weights must sum to 1.0, got {sum}"
);
}
#[test]
fn test_all_pass_gives_100() {
let attacks: Vec<AttackResult> = crate::attacks::all_tests()
.iter()
.map(|t| AttackResult {
attack_id: t.id.to_string(),
name: t.name.to_string(),
class: t.class.to_string(),
passed: true,
latency_ns: 0,
details: String::new(),
})
.collect();
let props = calculate_property_scores(&attacks);
let score = calculate_overall_score(&props);
assert!(
(score - 100.0).abs() < 0.01,
"All-pass should give 100%, got {score}"
);
}
#[test]
fn test_all_fail_gives_zero() {
let attacks: Vec<AttackResult> = crate::attacks::all_tests()
.iter()
.map(|t| AttackResult {
attack_id: t.id.to_string(),
name: t.name.to_string(),
class: t.class.to_string(),
passed: false,
latency_ns: 0,
details: String::new(),
})
.collect();
let props = calculate_property_scores(&attacks);
let score = calculate_overall_score(&props);
assert!(score.abs() < 0.01, "All-fail should give 0%, got {score}");
}
}