use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum RuleType {
ProtocolVersion,
CipherSuite,
CertificateKeySize,
SignatureAlgorithm,
ForwardSecrecy,
CertificateValidation,
CertificateExpiration,
Vulnerability,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Rule {
#[serde(rename = "type")]
pub rule_type: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub allowed: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub denied: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub allowed_patterns: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub denied_patterns: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub preferred_patterns: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub min_rsa_bits: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub min_ecc_bits: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub required: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub require_valid_chain: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub require_unexpired: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub require_hostname_match: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub max_days_until_expiration: Option<i64>,
#[serde(flatten)]
pub custom_params: HashMap<String, serde_yaml::Value>,
}
impl Rule {
pub fn is_allowed(&self, value: &str) -> bool {
if !self.allowed.is_empty() {
self.allowed.contains(&value.to_string())
} else {
true }
}
pub fn is_denied(&self, value: &str) -> bool {
if !self.denied.is_empty() {
self.denied.contains(&value.to_string())
} else {
false
}
}
pub fn matches_allowed_pattern(&self, value: &str) -> bool {
if self.allowed_patterns.is_empty() {
return true;
}
for pattern in &self.allowed_patterns {
if let Ok(re) = regex::Regex::new(pattern)
&& re.is_match(value)
{
return true;
}
}
false
}
pub fn matches_denied_pattern(&self, value: &str) -> bool {
for pattern in &self.denied_patterns {
if let Ok(re) = regex::Regex::new(pattern)
&& re.is_match(value)
{
return true;
}
}
false
}
pub fn matches_preferred_pattern(&self, value: &str) -> bool {
for pattern in &self.preferred_patterns {
if let Ok(re) = regex::Regex::new(pattern)
&& re.is_match(value)
{
return true;
}
}
false
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_rule_allowed_list() {
let rule = Rule {
rule_type: "ProtocolVersion".to_string(),
allowed: vec!["TLS 1.2".to_string(), "TLS 1.3".to_string()],
denied: vec![],
allowed_patterns: vec![],
denied_patterns: vec![],
preferred_patterns: vec![],
min_rsa_bits: None,
min_ecc_bits: None,
required: None,
require_valid_chain: None,
require_unexpired: None,
require_hostname_match: None,
max_days_until_expiration: None,
custom_params: HashMap::new(),
};
assert!(rule.is_allowed("TLS 1.2"));
assert!(rule.is_allowed("TLS 1.3"));
assert!(!rule.is_allowed("TLS 1.0"));
}
#[test]
fn test_rule_denied_list() {
let rule = Rule {
rule_type: "ProtocolVersion".to_string(),
allowed: vec![],
denied: vec!["SSLv2".to_string(), "SSLv3".to_string()],
allowed_patterns: vec![],
denied_patterns: vec![],
preferred_patterns: vec![],
min_rsa_bits: None,
min_ecc_bits: None,
required: None,
require_valid_chain: None,
require_unexpired: None,
require_hostname_match: None,
max_days_until_expiration: None,
custom_params: HashMap::new(),
};
assert!(rule.is_denied("SSLv2"));
assert!(rule.is_denied("SSLv3"));
assert!(!rule.is_denied("TLS 1.2"));
}
#[test]
fn test_rule_pattern_matching() {
let rule = Rule {
rule_type: "CipherSuite".to_string(),
allowed: vec![],
denied: vec![],
allowed_patterns: vec![],
denied_patterns: vec![".*_NULL_.*".to_string(), ".*_EXPORT_.*".to_string()],
preferred_patterns: vec![],
min_rsa_bits: None,
min_ecc_bits: None,
required: None,
require_valid_chain: None,
require_unexpired: None,
require_hostname_match: None,
max_days_until_expiration: None,
custom_params: HashMap::new(),
};
assert!(rule.matches_denied_pattern("TLS_RSA_WITH_NULL_SHA"));
assert!(rule.matches_denied_pattern("TLS_RSA_EXPORT_WITH_DES40_CBC_SHA"));
assert!(!rule.matches_denied_pattern("TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384"));
}
}