use lazy_static::lazy_static;
use regex::Regex;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
lazy_static! {
pub static ref PII_PATTERNS: Vec<Regex> = vec![
Regex::new(r"\b\d{4}[\s-]?\d{4}[\s-]?\d{4}[\s-]?\d{4}\b").unwrap(),
Regex::new(r"\b\d{3}-\d{2}-\d{4}\b").unwrap(),
Regex::new(r"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b").unwrap(),
Regex::new(r"\b\d{3}[-.]?\d{3}[-.]?\d{4}\b").unwrap(),
Regex::new(r"\(\d{3}\)\s?\d{3}[-.]?\d{4}").unwrap(),
Regex::new(r"(?i)(api[_-]?key|apikey|api_token)\s*[=:]\s*['\x22]?([a-zA-Z0-9_-]{20,})['\x22]?").unwrap(),
Regex::new(r"Bearer\s+[A-Za-z0-9\-._~+/]+=*").unwrap(),
];
pub static ref SENSITIVE_HEADERS: Vec<&'static str> = vec![
"authorization",
"cookie",
"set-cookie",
"x-api-key",
"x-auth-token",
"proxy-authorization",
];
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LoggingPolicy {
pub log_request_headers: bool,
pub log_response_headers: bool,
pub log_request_body: bool,
pub log_response_body: bool,
pub sampling_rate: f64,
pub enable_pii_redaction: bool,
pub encrypt_logs: bool,
}
impl Default for LoggingPolicy {
fn default() -> Self {
Self {
log_request_headers: false,
log_response_headers: false,
log_request_body: false,
log_response_body: false,
sampling_rate: 0.01, enable_pii_redaction: true,
encrypt_logs: true,
}
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct RequestMetadata {
pub timestamp: i64,
pub method: String,
pub host: String,
pub port: u16,
pub path: String,
pub http_version: String,
pub status_code: Option<u16>,
pub request_size: usize,
pub response_size: usize,
pub duration_ms: u64,
pub tls_version: Option<String>,
pub mitm_applied: bool,
pub bypass_reason: Option<String>,
}
pub struct PiiRedactor;
impl PiiRedactor {
pub fn redact(text: &str) -> String {
let mut redacted = text.to_string();
for pattern in PII_PATTERNS.iter() {
redacted = pattern.replace_all(&redacted, "[REDACTED]").to_string();
}
redacted
}
pub fn redact_headers(headers: &HashMap<String, String>) -> HashMap<String, String> {
let mut redacted = headers.clone();
for sensitive in SENSITIVE_HEADERS.iter() {
if redacted.contains_key(*sensitive) {
redacted.insert(sensitive.to_string(), "[REDACTED]".to_string());
}
}
redacted
}
pub fn should_sample(rate: f64) -> bool {
use rand::Rng;
let mut rng = rand::thread_rng();
rng.gen::<f64>() < rate
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_pii_redaction_credit_card() {
let text = "My card is 4532-1234-5678-9010";
let redacted = PiiRedactor::redact(text);
assert!(redacted.contains("[REDACTED]"));
assert!(!redacted.contains("4532"));
}
#[test]
fn test_pii_redaction_ssn() {
let text = "SSN: 123-45-6789";
let redacted = PiiRedactor::redact(text);
assert!(redacted.contains("[REDACTED]"));
assert!(!redacted.contains("123-45-6789"));
}
#[test]
fn test_pii_redaction_email() {
let text = "Contact: user@example.com";
let redacted = PiiRedactor::redact(text);
assert!(redacted.contains("[REDACTED]"));
assert!(!redacted.contains("user@example.com"));
}
#[test]
fn test_sensitive_header_redaction() {
let mut headers = HashMap::new();
headers.insert("authorization".to_string(), "Bearer token123".to_string());
headers.insert("content-type".to_string(), "application/json".to_string());
let redacted = PiiRedactor::redact_headers(&headers);
assert_eq!(redacted.get("authorization").unwrap(), "[REDACTED]");
assert_eq!(redacted.get("content-type").unwrap(), "application/json");
}
#[test]
fn test_pii_redaction_phone_numbers() {
let text1 = "Call me at 555-123-4567";
let redacted1 = PiiRedactor::redact(text1);
assert!(redacted1.contains("[REDACTED]"));
assert!(!redacted1.contains("555-123-4567"));
let text2 = "Phone: (555) 123-4567";
let redacted2 = PiiRedactor::redact(text2);
assert!(redacted2.contains("[REDACTED]"));
assert!(!redacted2.contains("(555) 123-4567"));
let text3 = "Contact: 5551234567";
let redacted3 = PiiRedactor::redact(text3);
assert!(redacted3.contains("[REDACTED]"));
assert!(!redacted3.contains("5551234567"));
}
#[test]
fn test_pii_redaction_api_keys() {
let text1 = "api_key=sk_live_51HqZ2KJ4Vr3sT7Y8pQwXyZ";
let redacted1 = PiiRedactor::redact(text1);
assert!(redacted1.contains("[REDACTED]"));
assert!(!redacted1.contains("sk_live_51HqZ2KJ4Vr3sT7Y8pQwXyZ"));
let text2 = "API-KEY: \"abcdef1234567890abcdef1234567890\"";
let redacted2 = PiiRedactor::redact(text2);
assert!(redacted2.contains("[REDACTED]"));
assert!(!redacted2.contains("abcdef1234567890abcdef1234567890"));
let text3 = "apikey='ghijklmnopqrstuvwxyz123456'";
let redacted3 = PiiRedactor::redact(text3);
assert!(redacted3.contains("[REDACTED]"));
assert!(!redacted3.contains("ghijklmnopqrstuvwxyz123456"));
}
#[test]
fn test_pii_redaction_bearer_tokens() {
let text = "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0";
let redacted = PiiRedactor::redact(text);
assert!(redacted.contains("[REDACTED]"));
assert!(!redacted.contains("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9"));
}
#[test]
fn test_multiple_pii_in_same_text() {
let text = "User email: user@example.com, SSN: 123-45-6789, Card: 4532-1234-5678-9010";
let redacted = PiiRedactor::redact(text);
assert!(!redacted.contains("user@example.com"));
assert!(!redacted.contains("123-45-6789"));
assert!(!redacted.contains("4532-1234-5678-9010"));
let redacted_count = redacted.matches("[REDACTED]").count();
assert!(
redacted_count >= 3,
"Expected at least 3 redactions, found {}",
redacted_count
);
}
#[test]
fn test_sampling_behavior() {
let mut sampled_count = 0;
for _ in 0..100 {
if PiiRedactor::should_sample(1.0) {
sampled_count += 1;
}
}
assert_eq!(sampled_count, 100);
sampled_count = 0;
for _ in 0..100 {
if PiiRedactor::should_sample(0.0) {
sampled_count += 1;
}
}
assert_eq!(sampled_count, 0);
sampled_count = 0;
for _ in 0..1000 {
if PiiRedactor::should_sample(0.1) {
sampled_count += 1;
}
}
assert!(
sampled_count >= 50 && sampled_count <= 150,
"Expected ~100 samples (50-150), got {}",
sampled_count
);
}
#[test]
fn test_no_false_positives() {
let text = "Invoice #1234-5678-9012-3456 dated 2023-11-24";
let redacted = PiiRedactor::redact(text);
assert!(redacted.contains("2023-11-24"));
}
#[test]
fn test_logging_policy_defaults() {
let policy = LoggingPolicy::default();
assert!(!policy.log_request_headers);
assert!(!policy.log_response_headers);
assert!(!policy.log_request_body);
assert!(!policy.log_response_body);
assert_eq!(policy.sampling_rate, 0.01);
assert!(policy.enable_pii_redaction);
assert!(policy.encrypt_logs);
}
}