use crate::certificates::validator::ValidationResult;
use crate::ciphers::tester::ProtocolCipherSummary;
use crate::protocols::{Protocol, ProtocolTestResult};
use crate::vulnerabilities::VulnerabilityResult;
use std::collections::HashMap;
use super::grader::Grade;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RatingResult {
pub grade: Grade,
pub score: u8, pub certificate_score: u8,
pub protocol_score: u8,
pub key_exchange_score: u8,
pub cipher_strength_score: u8,
pub warnings: Vec<String>,
}
pub struct RatingCalculator;
impl RatingCalculator {
pub fn calculate(
protocols: &[ProtocolTestResult],
ciphers: &HashMap<Protocol, ProtocolCipherSummary>,
certificate: Option<&ValidationResult>,
vulnerabilities: &[VulnerabilityResult],
) -> RatingResult {
let certificate_score = Self::calculate_certificate_score(certificate);
let protocol_score = Self::calculate_protocol_score(protocols);
let key_exchange_score = Self::calculate_key_exchange_score(ciphers);
let cipher_strength_score = Self::calculate_cipher_strength_score(ciphers);
let mut score = Self::calculate_overall_score(
protocol_score,
key_exchange_score,
cipher_strength_score,
);
let has_tls13 = protocols
.iter()
.any(|p| p.supported && p.protocol == Protocol::TLS13);
if !has_tls13 {
score = score.min(84);
}
let (score, warnings) = Self::apply_vulnerability_penalties(score, vulnerabilities);
let mut grade = Grade::from_score(score);
if let Some(cert) = certificate {
if !cert.hostname_match {
grade = Grade::M; } else if !cert.trust_chain_valid {
grade = Grade::T; }
}
RatingResult {
grade,
score,
certificate_score,
protocol_score,
key_exchange_score,
cipher_strength_score,
warnings,
}
}
fn calculate_certificate_score(certificate: Option<&ValidationResult>) -> u8 {
let Some(cert) = certificate else {
return 0;
};
let mut score = 100u8;
if !cert.not_expired {
return 0;
}
if !cert.hostname_match {
return 0;
}
if !cert.trust_chain_valid {
return 0;
}
use crate::certificates::validator::IssueSeverity;
for issue in &cert.issues {
let deduction = match issue.severity {
IssueSeverity::Critical => 40,
IssueSeverity::High => 20,
IssueSeverity::Medium => 10,
IssueSeverity::Low => 5,
IssueSeverity::Info => 0,
};
score = score.saturating_sub(deduction);
}
score
}
fn calculate_protocol_score(protocols: &[ProtocolTestResult]) -> u8 {
let mut score = 100u8;
for result in protocols {
if result.supported {
match result.protocol {
Protocol::SSLv2 => score = 0, Protocol::SSLv3 => score = score.saturating_sub(20), Protocol::TLS10 => score = score.saturating_sub(5), Protocol::TLS11 => score = score.saturating_sub(3), Protocol::TLS12 | Protocol::TLS13 => {} Protocol::QUIC => {} }
}
}
let has_modern_tls = protocols.iter().any(|p| {
p.supported && (p.protocol == Protocol::TLS12 || p.protocol == Protocol::TLS13)
});
if !has_modern_tls {
score = score.saturating_sub(20);
}
let has_tls13 = protocols
.iter()
.any(|p| p.supported && p.protocol == Protocol::TLS13);
if !has_tls13 {
score = score.saturating_sub(15);
}
score
}
fn calculate_key_exchange_score(ciphers: &HashMap<Protocol, ProtocolCipherSummary>) -> u8 {
let mut score = 100u8;
let mut total_ciphers = 0;
let mut fs_ciphers = 0;
for summary in ciphers.values() {
total_ciphers += summary.counts.total;
fs_ciphers += summary.counts.forward_secrecy;
}
if total_ciphers > 0 {
let fs_percentage = (fs_ciphers * 100) / total_ciphers;
let non_fs_percentage = 100 - fs_percentage;
if non_fs_percentage >= 50 {
score = score.saturating_sub(20);
} else if non_fs_percentage >= 30 {
score = score.saturating_sub(10);
} else if non_fs_percentage >= 20 {
score = score.saturating_sub(5);
}
}
score
}
fn calculate_cipher_strength_score(ciphers: &HashMap<Protocol, ProtocolCipherSummary>) -> u8 {
let mut score = 100u8;
let mut total_ciphers = 0;
let mut weak_ciphers = 0; let mut low_strength_count = 0;
let mut aead_count = 0;
for summary in ciphers.values() {
if summary.counts.null_ciphers > 0 {
return 0;
}
if summary.counts.export_ciphers > 0 {
return 0;
}
total_ciphers += summary.counts.total;
weak_ciphers += summary.counts.low_strength + summary.counts.medium_strength;
low_strength_count += summary.counts.low_strength;
aead_count += summary.counts.aead;
}
if total_ciphers > 0 {
let weak_percentage = (weak_ciphers * 100) / total_ciphers;
let low_percentage = (low_strength_count * 100) / total_ciphers;
if weak_percentage >= 75 {
score = score.saturating_sub(20);
} else if weak_percentage >= 50 {
score = score.saturating_sub(15);
} else if weak_percentage >= 25 {
score = score.saturating_sub(10);
} else if weak_percentage > 0 {
score = score.saturating_sub(5);
}
if low_percentage >= 25 {
score = score.saturating_sub(10);
} else if low_percentage > 0 {
score = score.saturating_sub(5);
}
let aead_percentage = (aead_count * 100) / total_ciphers;
if aead_percentage < 50 {
score = score.saturating_sub(5);
}
}
score
}
fn calculate_overall_score(protocol: u8, key_exchange: u8, cipher: u8) -> u8 {
let weighted = (protocol as u32 * 30 + key_exchange as u32 * 30 + cipher as u32 * 40) / 100;
weighted.min(100) as u8
}
fn apply_vulnerability_penalties(
mut score: u8,
vulnerabilities: &[VulnerabilityResult],
) -> (u8, Vec<String>) {
let mut warnings = Vec::new();
for vuln in vulnerabilities {
if matches!(
vuln.vuln_type,
crate::vulnerabilities::VulnerabilityType::TLSFallback
) {
continue;
}
if vuln.vulnerable {
use crate::vulnerabilities::Severity;
let (deduction, warning) = match vuln.severity {
Severity::Critical => {
(40, format!("Critical vulnerability: {:?}", vuln.vuln_type))
}
Severity::High => (
20,
format!("High severity vulnerability: {:?}", vuln.vuln_type),
),
Severity::Medium => (
10,
format!("Medium severity vulnerability: {:?}", vuln.vuln_type),
),
Severity::Low => (
5,
format!("Low severity vulnerability: {:?}", vuln.vuln_type),
),
Severity::Info => (0, String::new()),
};
score = score.saturating_sub(deduction);
if !warning.is_empty() {
warnings.push(warning);
}
}
}
(score, warnings)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_overall_score_calculation() {
let score = RatingCalculator::calculate_overall_score(100, 100, 100);
assert_eq!(score, 100);
let score = RatingCalculator::calculate_overall_score(90, 85, 95);
assert_eq!(score, 90);
let score = RatingCalculator::calculate_overall_score(100, 95, 95);
assert_eq!(score, 96);
}
#[test]
fn test_certificate_score_expired() {
}
#[test]
fn test_protocol_score_with_tls13() {
let protocols = vec![
ProtocolTestResult {
protocol: Protocol::TLS12,
supported: true,
preferred: false,
ciphers_count: 10,
handshake_time_ms: None,
heartbeat_enabled: None,
session_resumption_caching: None,
session_resumption_tickets: None,
secure_renegotiation: None,
},
ProtocolTestResult {
protocol: Protocol::TLS13,
supported: true,
preferred: true,
ciphers_count: 5,
handshake_time_ms: None,
heartbeat_enabled: None,
session_resumption_caching: None,
session_resumption_tickets: None,
secure_renegotiation: None,
},
];
let score = RatingCalculator::calculate_protocol_score(&protocols);
assert_eq!(score, 100, "Server with TLS 1.3 should get full score");
}
#[test]
fn test_protocol_score_without_tls13_gets_penalty() {
let protocols = vec![ProtocolTestResult {
protocol: Protocol::TLS12,
supported: true,
preferred: true,
ciphers_count: 10,
handshake_time_ms: None,
heartbeat_enabled: None,
session_resumption_caching: None,
session_resumption_tickets: None,
secure_renegotiation: None,
}];
let score = RatingCalculator::calculate_protocol_score(&protocols);
assert_eq!(
score, 85,
"Server without TLS 1.3 should get -15 penalty (100-15=85)"
);
}
#[test]
fn test_protocol_score_without_tls13_with_old_tls() {
let protocols = vec![
ProtocolTestResult {
protocol: Protocol::TLS10,
supported: true,
preferred: false,
ciphers_count: 20,
handshake_time_ms: None,
heartbeat_enabled: None,
session_resumption_caching: None,
session_resumption_tickets: None,
secure_renegotiation: None,
},
ProtocolTestResult {
protocol: Protocol::TLS11,
supported: true,
preferred: false,
ciphers_count: 15,
handshake_time_ms: None,
heartbeat_enabled: None,
session_resumption_caching: None,
session_resumption_tickets: None,
secure_renegotiation: None,
},
ProtocolTestResult {
protocol: Protocol::TLS12,
supported: true,
preferred: true,
ciphers_count: 10,
handshake_time_ms: None,
heartbeat_enabled: None,
session_resumption_caching: None,
session_resumption_tickets: None,
secure_renegotiation: None,
},
];
let score = RatingCalculator::calculate_protocol_score(&protocols);
assert_eq!(
score, 77,
"Server without TLS 1.3 gets cumulative penalties"
);
}
#[test]
fn test_protocol_score_sslv3_with_penalties() {
let protocols = vec![
ProtocolTestResult {
protocol: Protocol::SSLv3,
supported: true,
preferred: false,
ciphers_count: 50,
handshake_time_ms: None,
heartbeat_enabled: None,
session_resumption_caching: None,
session_resumption_tickets: None,
secure_renegotiation: None,
},
ProtocolTestResult {
protocol: Protocol::TLS12,
supported: true,
preferred: true,
ciphers_count: 10,
handshake_time_ms: None,
heartbeat_enabled: None,
session_resumption_caching: None,
session_resumption_tickets: None,
secure_renegotiation: None,
},
];
let score = RatingCalculator::calculate_protocol_score(&protocols);
assert_eq!(
score, 65,
"Server with SSLv3 and no TLS 1.3 gets heavy penalties"
);
}
#[test]
fn test_protocol_score_sslv2_instant_fail() {
let protocols = vec![
ProtocolTestResult {
protocol: Protocol::SSLv2,
supported: true,
preferred: false,
ciphers_count: 20,
handshake_time_ms: None,
heartbeat_enabled: None,
session_resumption_caching: None,
session_resumption_tickets: None,
secure_renegotiation: None,
},
ProtocolTestResult {
protocol: Protocol::TLS13,
supported: true,
preferred: true,
ciphers_count: 5,
handshake_time_ms: None,
heartbeat_enabled: None,
session_resumption_caching: None,
session_resumption_tickets: None,
secure_renegotiation: None,
},
];
let score = RatingCalculator::calculate_protocol_score(&protocols);
assert_eq!(score, 0, "SSLv2 support should result in instant fail");
}
#[test]
fn test_grade_conversion_with_tls13_cap() {
let grade = Grade::from_score(84);
assert_eq!(grade, Grade::AMinus, "Score 84 should be grade A-");
let grade = Grade::from_score(85);
assert_eq!(grade, Grade::A, "Score 85 should be grade A");
let grade = Grade::from_score(100);
assert_eq!(grade, Grade::APlus, "Score 100 should be grade A+");
}
#[test]
fn test_user_reported_issue_fix() {
use std::collections::HashMap;
let protocols_with_tls13 = vec![
ProtocolTestResult {
protocol: Protocol::TLS12,
supported: true,
preferred: false,
ciphers_count: 10,
handshake_time_ms: None,
heartbeat_enabled: None,
session_resumption_caching: None,
session_resumption_tickets: None,
secure_renegotiation: None,
},
ProtocolTestResult {
protocol: Protocol::TLS13,
supported: true,
preferred: true,
ciphers_count: 5,
handshake_time_ms: None,
heartbeat_enabled: None,
session_resumption_caching: None,
session_resumption_tickets: None,
secure_renegotiation: None,
},
];
let ciphers = HashMap::new();
let vulnerabilities = vec![crate::vulnerabilities::VulnerabilityResult {
vuln_type: crate::vulnerabilities::VulnerabilityType::TLSFallback,
vulnerable: true, details: "TLS_FALLBACK_SCSV not supported".to_string(),
cve: Some("CVE-2014-8730".to_string()),
cwe: Some("CWE-757".to_string()),
severity: crate::vulnerabilities::Severity::High,
}];
let result =
RatingCalculator::calculate(&protocols_with_tls13, &ciphers, None, &vulnerabilities);
assert!(
result.score >= 95,
"Score should be 96 or close (got {})",
result.score
);
assert_eq!(
result.grade,
Grade::APlus,
"Grade should be A+ with TLS 1.3 and score 96"
);
}
#[test]
fn test_tls13_overall_score_cap() {
use std::collections::HashMap;
let protocols_without_tls13 = vec![ProtocolTestResult {
protocol: Protocol::TLS12,
supported: true,
preferred: true,
ciphers_count: 10,
handshake_time_ms: None,
heartbeat_enabled: None,
session_resumption_caching: None,
session_resumption_tickets: None,
secure_renegotiation: None,
}];
let ciphers = HashMap::new();
let vulnerabilities = vec![];
let result =
RatingCalculator::calculate(&protocols_without_tls13, &ciphers, None, &vulnerabilities);
assert_eq!(
result.score, 84,
"Overall score should be capped at 84 without TLS 1.3"
);
assert_eq!(
result.grade,
Grade::AMinus,
"Grade should be A- without TLS 1.3"
);
}
}