use serde::{Deserialize, Serialize};
use crate::Error;
pub const DEFAULT_RULES_TOML: &str = include_str!("../rules/default.toml");
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum Category {
Pii,
Secret,
Injection,
}
impl Category {
pub fn label(self) -> &'static str {
match self {
Category::Pii => "PII",
Category::Secret => "SECRET",
Category::Injection => "INJECTION",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Severity {
Low,
Medium,
High,
Critical,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum Validator {
Luhn,
}
impl Validator {
pub fn accepts(self, matched: &str) -> bool {
match self {
Validator::Luhn => luhn(matched),
}
}
}
fn luhn(s: &str) -> bool {
let digits: Vec<u32> = s.chars().filter_map(|c| c.to_digit(10)).collect();
if !(13..=19).contains(&digits.len()) {
return false;
}
let sum: u32 = digits
.iter()
.rev()
.enumerate()
.map(|(i, &d)| {
if i % 2 == 1 {
let dd = d * 2;
if dd > 9 { dd - 9 } else { dd }
} else {
d
}
})
.sum();
sum % 10 == 0
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Rule {
pub id: String,
pub category: Category,
pub severity: Severity,
#[serde(default)]
pub description: String,
pub pattern: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub validator: Option<Validator>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub redact_with: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(default)]
pub struct Policy {
pub block_at: Severity,
pub warn_at: Severity,
pub redact: Vec<Category>,
}
impl Default for Policy {
fn default() -> Self {
Self {
block_at: Severity::High,
warn_at: Severity::Medium,
redact: vec![Category::Pii, Category::Secret],
}
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(default)]
pub struct RuleSet {
pub policy: Policy,
pub rules: Vec<Rule>,
}
impl RuleSet {
pub fn default_rules() -> Self {
Self::from_toml(DEFAULT_RULES_TOML).expect("bundled rules must parse")
}
pub fn from_toml(toml_text: &str) -> Result<Self, Error> {
Ok(toml::from_str(toml_text)?)
}
pub fn merge(&mut self, other: RuleSet) {
self.policy = other.policy;
self.rules.extend(other.rules);
}
pub(crate) fn validate_ids(&self) -> Result<(), Error> {
let mut seen = std::collections::HashSet::new();
for (i, rule) in self.rules.iter().enumerate() {
if rule.id.is_empty() {
return Err(Error::EmptyRuleId(i));
}
if !seen.insert(rule.id.as_str()) {
return Err(Error::DuplicateRuleId(rule.id.clone()));
}
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn luhn_accepts_known_test_numbers() {
assert!(luhn("4111 1111 1111 1111"));
assert!(luhn("5555-5555-5555-4444"));
assert!(luhn("378282246310005"));
}
#[test]
fn luhn_rejects_bad_checksum_and_wrong_length() {
assert!(!luhn("4111 1111 1111 1112"));
assert!(!luhn("1234"));
assert!(!luhn("12345678901234567890"));
}
#[test]
fn default_rules_parse_and_have_unique_ids() {
let rs = RuleSet::default_rules();
assert!(rs.rules.len() >= 10);
rs.validate_ids().unwrap();
}
#[test]
fn severity_is_ordered() {
assert!(Severity::Low < Severity::Medium);
assert!(Severity::Medium < Severity::High);
assert!(Severity::High < Severity::Critical);
}
#[test]
fn duplicate_ids_are_rejected() {
let rs = RuleSet::from_toml(
r#"
[[rules]]
id = "a"
category = "pii"
severity = "low"
pattern = "x"
[[rules]]
id = "a"
category = "pii"
severity = "low"
pattern = "y"
"#,
)
.unwrap();
assert!(matches!(rs.validate_ids(), Err(Error::DuplicateRuleId(id)) if id == "a"));
}
}