use regex::Regex;
use chio_kernel::{Guard, GuardContext, GuardDecision, KernelError};
use super::detectors::compile_required_pattern;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SensitivityLevel {
Low,
Medium,
High,
}
#[derive(Debug, Clone)]
pub struct SensitivePattern {
pub name: String,
regex: Regex,
pub level: SensitivityLevel,
pub redaction: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SanitizationAction {
Block,
Redact,
}
pub(super) fn default_patterns() -> Vec<SensitivePattern> {
let specs: [(&str, &str, SensitivityLevel, &str); 7] = [
(
r"\b\d{3}-\d{2}-\d{4}\b",
"SSN",
SensitivityLevel::High,
"[SSN REDACTED]",
),
(
r"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b",
"email",
SensitivityLevel::Medium,
"[EMAIL REDACTED]",
),
(
r"\b(?:\(\d{3}\)\s*|\d{3}[-.])\d{3}[-.]?\d{4}\b",
"phone",
SensitivityLevel::Low,
"[PHONE REDACTED]",
),
(
r"\b(?:\d{4}[-\s]?){3}\d{4}\b",
"credit-card",
SensitivityLevel::High,
"[CARD REDACTED]",
),
(
r"\b(?:\d{2}/\d{2}/\d{4}|\d{4}-\d{2}-\d{2})\b",
"date-of-birth",
SensitivityLevel::Low,
"[DATE REDACTED]",
),
(
r"\bMRN[:\s#]*\d{6,12}\b",
"MRN",
SensitivityLevel::High,
"[MRN REDACTED]",
),
(
r"\b[A-Z]\d{2}(?:\.\d{1,4})?\b",
"ICD-10",
SensitivityLevel::Medium,
"[ICD REDACTED]",
),
];
let mut patterns = Vec::with_capacity(specs.len());
for (pattern, name, level, redaction) in specs {
match compile_required_pattern(pattern) {
Ok(regex) => patterns.push(SensitivePattern {
name: name.to_string(),
regex,
level,
redaction: redaction.to_string(),
}),
Err(_) => {
if let Ok(catch_all) = compile_required_pattern(r"[\s\S]+") {
return vec![SensitivePattern {
name: "redaction_unavailable_fail_closed".to_string(),
regex: catch_all,
level: SensitivityLevel::High,
redaction: "[REDACTED]".to_string(),
}];
}
return patterns;
}
}
}
patterns
}
pub struct ResponseSanitizationGuard {
patterns: Vec<SensitivePattern>,
min_level: SensitivityLevel,
action: SanitizationAction,
}
impl ResponseSanitizationGuard {
pub fn new(min_level: SensitivityLevel, action: SanitizationAction) -> Self {
Self {
patterns: default_patterns(),
min_level,
action,
}
}
pub fn with_patterns(
patterns: Vec<SensitivePattern>,
min_level: SensitivityLevel,
action: SanitizationAction,
) -> Self {
Self {
patterns,
min_level,
action,
}
}
pub fn with_additional_patterns(
additional_patterns: Vec<SensitivePattern>,
min_level: SensitivityLevel,
action: SanitizationAction,
) -> Self {
let mut patterns = default_patterns();
patterns.extend(additional_patterns);
Self {
patterns,
min_level,
action,
}
}
pub fn scan(&self, text: &str) -> Vec<(String, String)> {
let mut findings = Vec::new();
for pattern in &self.patterns {
if level_ord(pattern.level) < level_ord(self.min_level) {
continue;
}
for m in pattern.regex.find_iter(text) {
findings.push((pattern.name.clone(), m.as_str().to_string()));
}
}
findings
}
pub fn redact(&self, text: &str) -> (String, usize) {
let mut result = text.to_string();
let mut count = 0usize;
for pattern in &self.patterns {
if level_ord(pattern.level) < level_ord(self.min_level) {
continue;
}
let match_count = pattern.regex.find_iter(&result).count();
if match_count > 0 {
result = pattern
.regex
.replace_all(&result, pattern.redaction.as_str())
.to_string();
count = count.saturating_add(match_count);
}
}
(result, count)
}
pub fn scan_response(&self, response: &serde_json::Value) -> ScanResult {
let text = response.to_string();
let findings = self.scan(&text);
if findings.is_empty() {
return ScanResult::Clean;
}
match self.action {
SanitizationAction::Block => ScanResult::Blocked(findings),
SanitizationAction::Redact => {
let (redacted, count) = self.redact(&text);
ScanResult::Redacted {
redacted_text: redacted,
redaction_count: count,
findings,
}
}
}
}
}
#[derive(Debug)]
pub enum ScanResult {
Clean,
Blocked(Vec<(String, String)>),
Redacted {
redacted_text: String,
redaction_count: usize,
findings: Vec<(String, String)>,
},
}
fn level_ord(level: SensitivityLevel) -> u8 {
match level {
SensitivityLevel::Low => 0,
SensitivityLevel::Medium => 1,
SensitivityLevel::High => 2,
}
}
impl Guard for ResponseSanitizationGuard {
fn name(&self) -> &str {
"response-sanitization"
}
fn evaluate(&self, ctx: &GuardContext) -> Result<GuardDecision, KernelError> {
let args_text = ctx.request.arguments.to_string();
let findings = self.scan(&args_text);
if findings.is_empty() {
Ok(GuardDecision::allow())
} else {
Ok(GuardDecision::deny(Vec::new()))
}
}
}
pub fn build_pattern(
name: &str,
regex_str: &str,
level: SensitivityLevel,
redaction: &str,
) -> Option<SensitivePattern> {
Regex::new(regex_str).ok().map(|regex| SensitivePattern {
name: name.to_string(),
regex,
level,
redaction: redaction.to_string(),
})
}