Skip to main content

ranvier_compliance/
lib.rs

1use chrono::{DateTime, Utc};
2use serde::{Deserialize, Deserializer, Serialize, Serializer};
3use std::fmt;
4
5// ---------------------------------------------------------------------------
6// ClassificationLevel
7// ---------------------------------------------------------------------------
8
9/// Data classification level for compliance policies.
10///
11/// Higher levels require more stringent access controls and handling procedures.
12#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
13pub enum ClassificationLevel {
14    /// Freely shareable data.
15    Public,
16    /// Organization-internal data, not for public distribution.
17    Internal,
18    /// Sensitive data requiring access controls (e.g., financial records).
19    Confidential,
20    /// Highly sensitive data (e.g., PII, PHI, credentials). Requires explicit grant.
21    Restricted,
22}
23
24impl fmt::Display for ClassificationLevel {
25    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
26        match self {
27            ClassificationLevel::Public => write!(f, "Public"),
28            ClassificationLevel::Internal => write!(f, "Internal"),
29            ClassificationLevel::Confidential => write!(f, "Confidential"),
30            ClassificationLevel::Restricted => write!(f, "Restricted"),
31        }
32    }
33}
34
35// ---------------------------------------------------------------------------
36// Redact trait
37// ---------------------------------------------------------------------------
38
39/// Defines types that contain sensitive PII or PHI data and should be redacted in logs or regular outputs.
40pub trait Redact {
41    fn redact(&self) -> String;
42}
43
44// ---------------------------------------------------------------------------
45// Sensitive<T>
46// ---------------------------------------------------------------------------
47
48/// A wrapper for sensitive data indicating it falls under GDPR or HIPAA compliance scope.
49/// The inner data is strictly prevented from being debug-printed or logged by default.
50#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
51pub struct Sensitive<T> {
52    value: T,
53    /// Data classification level. Defaults to `Restricted`.
54    pub classification: ClassificationLevel,
55}
56
57impl<T> Sensitive<T> {
58    pub fn new(value: T) -> Self {
59        Self {
60            value,
61            classification: ClassificationLevel::Restricted,
62        }
63    }
64
65    /// Create a Sensitive wrapper with a specific classification level.
66    pub fn with_classification(value: T, classification: ClassificationLevel) -> Self {
67        Self {
68            value,
69            classification,
70        }
71    }
72
73    /// Explicitly unwrap and access the sensitive data. Use with caution.
74    pub fn expose(&self) -> &T {
75        &self.value
76    }
77
78    pub fn into_inner(self) -> T {
79        self.value
80    }
81}
82
83// Redact by default in Debug
84impl<T> fmt::Debug for Sensitive<T> {
85    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
86        write!(f, "[REDACTED:{}]", self.classification)
87    }
88}
89
90// Redact by default in Display
91impl<T> fmt::Display for Sensitive<T> {
92    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
93        write!(f, "[REDACTED]")
94    }
95}
96
97// Implement standard transparent Serialization (assume transmission over TLS is secure)
98impl<T: Serialize> Serialize for Sensitive<T> {
99    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
100    where
101        S: Serializer,
102    {
103        self.value.serialize(serializer)
104    }
105}
106
107impl<'de, T: Deserialize<'de>> Deserialize<'de> for Sensitive<T> {
108    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
109    where
110        D: Deserializer<'de>,
111    {
112        T::deserialize(deserializer).map(Sensitive::new)
113    }
114}
115
116// ---------------------------------------------------------------------------
117// EncryptionHook
118// ---------------------------------------------------------------------------
119
120/// Hook for field-level encryption/decryption of sensitive data.
121pub trait EncryptionHook: Send + Sync {
122    /// Encrypt the given plaintext bytes.
123    fn encrypt(&self, data: &[u8]) -> Vec<u8>;
124
125    /// Decrypt the given ciphertext bytes.
126    fn decrypt(&self, data: &[u8]) -> Vec<u8>;
127}
128
129/// No-op encryption hook that passes data through unchanged.
130pub struct NoOpEncryption;
131
132impl EncryptionHook for NoOpEncryption {
133    fn encrypt(&self, data: &[u8]) -> Vec<u8> {
134        data.to_vec()
135    }
136
137    fn decrypt(&self, data: &[u8]) -> Vec<u8> {
138        data.to_vec()
139    }
140}
141
142/// XOR-based encryption hook for testing/demo purposes.
143/// NOT SUITABLE FOR PRODUCTION — use AES-GCM or similar for real encryption.
144pub struct XorEncryption {
145    key: u8,
146}
147
148impl XorEncryption {
149    pub fn new(key: u8) -> Self {
150        Self { key }
151    }
152}
153
154impl EncryptionHook for XorEncryption {
155    fn encrypt(&self, data: &[u8]) -> Vec<u8> {
156        data.iter().map(|b| b ^ self.key).collect()
157    }
158
159    fn decrypt(&self, data: &[u8]) -> Vec<u8> {
160        // XOR is its own inverse
161        self.encrypt(data)
162    }
163}
164
165// ---------------------------------------------------------------------------
166// PII Detection
167// ---------------------------------------------------------------------------
168
169/// A detected PII field with its classification.
170#[derive(Debug, Clone, Serialize, Deserialize)]
171pub struct PiiField {
172    /// The JSON path or field name that was flagged.
173    pub field_name: String,
174    /// The suggested classification level.
175    pub classification: ClassificationLevel,
176    /// The PII category that triggered the match.
177    pub category: String,
178}
179
180/// Detects PII in field names using pattern matching.
181pub struct FieldNamePiiDetector {
182    patterns: Vec<(Vec<&'static str>, &'static str, ClassificationLevel)>,
183}
184
185impl Default for FieldNamePiiDetector {
186    fn default() -> Self {
187        Self::new()
188    }
189}
190
191impl FieldNamePiiDetector {
192    pub fn new() -> Self {
193        Self {
194            patterns: vec![
195                (
196                    vec!["email", "e_mail", "email_address"],
197                    "email",
198                    ClassificationLevel::Confidential,
199                ),
200                (
201                    vec!["phone", "phone_number", "mobile", "tel", "telephone"],
202                    "phone",
203                    ClassificationLevel::Confidential,
204                ),
205                (
206                    vec!["ssn", "social_security", "social_security_number"],
207                    "ssn",
208                    ClassificationLevel::Restricted,
209                ),
210                (
211                    vec![
212                        "address", "street", "street_address", "home_address",
213                        "postal_code", "zip_code", "zip",
214                    ],
215                    "address",
216                    ClassificationLevel::Confidential,
217                ),
218                (
219                    vec![
220                        "first_name", "last_name", "full_name", "given_name",
221                        "family_name", "surname",
222                    ],
223                    "name",
224                    ClassificationLevel::Confidential,
225                ),
226                (
227                    vec!["ip", "ip_address", "ipv4", "ipv6", "client_ip", "remote_addr"],
228                    "ip_address",
229                    ClassificationLevel::Internal,
230                ),
231                (
232                    vec![
233                        "credit_card", "card_number", "cc_number", "pan",
234                        "payment_card",
235                    ],
236                    "credit_card",
237                    ClassificationLevel::Restricted,
238                ),
239                (
240                    vec!["password", "passwd", "secret", "api_key", "access_token"],
241                    "credential",
242                    ClassificationLevel::Restricted,
243                ),
244                (
245                    vec!["date_of_birth", "dob", "birth_date", "birthday"],
246                    "date_of_birth",
247                    ClassificationLevel::Confidential,
248                ),
249                // Korean PII patterns
250                (
251                    vec!["jumin", "jumin_number", "resident_number", "resident_registration"],
252                    "kr_resident_number",
253                    ClassificationLevel::Restricted,
254                ),
255                (
256                    vec!["business_number", "saeopja", "business_registration"],
257                    "kr_business_number",
258                    ClassificationLevel::Confidential,
259                ),
260                (
261                    vec!["passport", "passport_number", "yeokkwon"],
262                    "passport",
263                    ClassificationLevel::Restricted,
264                ),
265                (
266                    vec!["drivers_license", "driver_license", "license_number", "myeonheo"],
267                    "drivers_license",
268                    ClassificationLevel::Restricted,
269                ),
270            ],
271        }
272    }
273
274    /// Classify a field name, returning the classification level if it matches a PII pattern.
275    pub fn classify(&self, field_name: &str) -> Option<ClassificationLevel> {
276        let lower = field_name.to_lowercase();
277        for (patterns, _, level) in &self.patterns {
278            if patterns.iter().any(|p| lower == *p || lower.contains(p)) {
279                return Some(*level);
280            }
281        }
282        None
283    }
284
285    /// Scan a JSON value for PII field names, returning all detected PII fields.
286    pub fn scan_value(&self, value: &serde_json::Value) -> Vec<PiiField> {
287        let mut results = Vec::new();
288        self.scan_recursive(value, "", &mut results);
289        results
290    }
291
292    fn scan_recursive(
293        &self,
294        value: &serde_json::Value,
295        path: &str,
296        results: &mut Vec<PiiField>,
297    ) {
298        match value {
299            serde_json::Value::Object(map) => {
300                for (key, val) in map {
301                    let field_path = if path.is_empty() {
302                        key.clone()
303                    } else {
304                        format!("{path}.{key}")
305                    };
306
307                    let lower = key.to_lowercase();
308                    for (patterns, category, level) in &self.patterns {
309                        if patterns.iter().any(|p| lower == *p || lower.contains(p)) {
310                            results.push(PiiField {
311                                field_name: field_path.clone(),
312                                classification: *level,
313                                category: category.to_string(),
314                            });
315                            break;
316                        }
317                    }
318
319                    self.scan_recursive(val, &field_path, results);
320                }
321            }
322            serde_json::Value::Array(arr) => {
323                for (i, val) in arr.iter().enumerate() {
324                    let item_path = format!("{path}[{i}]");
325                    self.scan_recursive(val, &item_path, results);
326                }
327            }
328            _ => {}
329        }
330    }
331}
332
333/// Trait for PII detection on text content.
334pub trait PiiDetector {
335    /// Detects if the given text likely contains Personally Identifiable Information
336    fn contains_pii(&self, text: &str) -> bool;
337}
338
339impl PiiDetector for FieldNamePiiDetector {
340    fn contains_pii(&self, text: &str) -> bool {
341        self.classify(text).is_some()
342    }
343}
344
345// ---------------------------------------------------------------------------
346// Right-to-Erasure (GDPR Article 17)
347// ---------------------------------------------------------------------------
348
349/// A request to erase personal data for a specific subject.
350#[derive(Debug, Clone, Serialize, Deserialize)]
351pub struct ErasureRequest {
352    /// Unique identifier for this erasure request.
353    pub id: String,
354    /// The data subject identifier (e.g., user ID, email).
355    pub subject: String,
356    /// Scope of erasure (e.g., "all", "transactions", specific table names).
357    pub scope: Vec<String>,
358    /// When the request was made.
359    pub timestamp: DateTime<Utc>,
360    /// Optional reason for the request.
361    pub reason: Option<String>,
362}
363
364impl ErasureRequest {
365    pub fn new(id: String, subject: String, scope: Vec<String>) -> Self {
366        Self {
367            id,
368            subject,
369            scope,
370            timestamp: Utc::now(),
371            reason: None,
372        }
373    }
374
375    pub fn with_reason(mut self, reason: impl Into<String>) -> Self {
376        self.reason = Some(reason.into());
377        self
378    }
379}
380
381/// Result of processing an erasure request.
382#[derive(Debug, Clone, Serialize, Deserialize)]
383pub struct ErasureResult {
384    /// The original request ID.
385    pub request_id: String,
386    /// Whether the erasure was successfully completed.
387    pub success: bool,
388    /// Number of records erased.
389    pub records_erased: usize,
390    /// Any scopes that could not be processed.
391    pub failed_scopes: Vec<String>,
392    /// When the erasure was completed.
393    pub completed_at: DateTime<Utc>,
394}
395
396/// Sink for processing data erasure requests.
397pub trait ErasureSink: Send + Sync {
398    /// Process a data erasure request.
399    fn erase(&self, request: &ErasureRequest) -> ErasureResult;
400}
401
402/// In-memory erasure sink for testing.
403pub struct InMemoryErasureSink {
404    records: std::sync::Mutex<std::collections::HashMap<String, Vec<String>>>,
405}
406
407impl Default for InMemoryErasureSink {
408    fn default() -> Self {
409        Self::new()
410    }
411}
412
413impl InMemoryErasureSink {
414    pub fn new() -> Self {
415        Self {
416            records: std::sync::Mutex::new(std::collections::HashMap::new()),
417        }
418    }
419
420    /// Add records for a subject (for testing purposes).
421    pub fn add_records(&self, subject: &str, data: Vec<String>) {
422        let mut records = self.records.lock().unwrap();
423        records.insert(subject.to_string(), data);
424    }
425}
426
427impl ErasureSink for InMemoryErasureSink {
428    fn erase(&self, request: &ErasureRequest) -> ErasureResult {
429        let mut records = self.records.lock().unwrap();
430        let count = if let Some(data) = records.remove(&request.subject) {
431            data.len()
432        } else {
433            0
434        };
435
436        ErasureResult {
437            request_id: request.id.clone(),
438            success: true,
439            records_erased: count,
440            failed_scopes: Vec::new(),
441            completed_at: Utc::now(),
442        }
443    }
444}
445
446#[cfg(test)]
447mod tests {
448    use super::*;
449
450    #[test]
451    fn test_sensitive_redaction() {
452        let email = Sensitive::new("user@example.com".to_string());
453
454        assert_eq!(format!("{:?}", email), "[REDACTED:Restricted]");
455        assert_eq!(format!("{}", email), "[REDACTED]");
456        assert_eq!(email.expose(), "user@example.com");
457    }
458
459    #[test]
460    fn test_sensitive_with_classification() {
461        let data = Sensitive::with_classification("internal doc", ClassificationLevel::Internal);
462        assert_eq!(data.classification, ClassificationLevel::Internal);
463        assert_eq!(format!("{:?}", data), "[REDACTED:Internal]");
464    }
465
466    #[test]
467    fn test_sensitive_serialization() {
468        let password = Sensitive::new("my_secret_pass".to_string());
469
470        let json = serde_json::to_string(&password).unwrap();
471        assert_eq!(json, "\"my_secret_pass\"");
472
473        let deserialized: Sensitive<String> = serde_json::from_str(&json).unwrap();
474        assert_eq!(deserialized.expose(), "my_secret_pass");
475    }
476
477    #[test]
478    fn classification_level_ordering() {
479        assert!(ClassificationLevel::Public < ClassificationLevel::Internal);
480        assert!(ClassificationLevel::Internal < ClassificationLevel::Confidential);
481        assert!(ClassificationLevel::Confidential < ClassificationLevel::Restricted);
482    }
483
484    #[test]
485    fn noop_encryption_passthrough() {
486        let hook = NoOpEncryption;
487        let data = b"hello world";
488        let encrypted = hook.encrypt(data);
489        let decrypted = hook.decrypt(&encrypted);
490        assert_eq!(decrypted, data);
491    }
492
493    #[test]
494    fn xor_encryption_roundtrip() {
495        let hook = XorEncryption::new(0x42);
496        let data = b"sensitive data";
497        let encrypted = hook.encrypt(data);
498        assert_ne!(encrypted, data, "Encrypted should differ from plaintext");
499        let decrypted = hook.decrypt(&encrypted);
500        assert_eq!(decrypted, data, "Decrypted should match original");
501    }
502
503    #[test]
504    fn pii_detector_classifies_email() {
505        let detector = FieldNamePiiDetector::new();
506        assert_eq!(
507            detector.classify("email"),
508            Some(ClassificationLevel::Confidential)
509        );
510        assert_eq!(
511            detector.classify("email_address"),
512            Some(ClassificationLevel::Confidential)
513        );
514    }
515
516    #[test]
517    fn pii_detector_classifies_ssn_as_restricted() {
518        let detector = FieldNamePiiDetector::new();
519        assert_eq!(
520            detector.classify("ssn"),
521            Some(ClassificationLevel::Restricted)
522        );
523        assert_eq!(
524            detector.classify("social_security_number"),
525            Some(ClassificationLevel::Restricted)
526        );
527    }
528
529    #[test]
530    fn pii_detector_no_match_for_regular_fields() {
531        let detector = FieldNamePiiDetector::new();
532        assert_eq!(detector.classify("created_at"), None);
533        assert_eq!(detector.classify("status"), None);
534        assert_eq!(detector.classify("quantity"), None);
535    }
536
537    #[test]
538    fn pii_detector_scan_json_value() {
539        let detector = FieldNamePiiDetector::new();
540        let json = serde_json::json!({
541            "id": 1,
542            "email": "test@example.com",
543            "profile": {
544                "first_name": "Alice",
545                "phone": "555-1234"
546            },
547            "status": "active"
548        });
549
550        let results = detector.scan_value(&json);
551        assert_eq!(results.len(), 3);
552
553        let field_names: Vec<&str> = results.iter().map(|f| f.field_name.as_str()).collect();
554        assert!(field_names.contains(&"email"));
555        assert!(field_names.contains(&"profile.first_name"));
556        assert!(field_names.contains(&"profile.phone"));
557    }
558
559    #[test]
560    fn pii_detector_trait_impl() {
561        let detector = FieldNamePiiDetector::new();
562        assert!(detector.contains_pii("email"));
563        assert!(!detector.contains_pii("status"));
564    }
565
566    #[test]
567    fn erasure_request_processing() {
568        let sink = InMemoryErasureSink::new();
569        sink.add_records(
570            "user_42",
571            vec![
572                "record1".into(),
573                "record2".into(),
574                "record3".into(),
575            ],
576        );
577
578        let request = ErasureRequest::new(
579            "req_001".into(),
580            "user_42".into(),
581            vec!["all".into()],
582        )
583        .with_reason("GDPR Article 17 request");
584
585        let result = sink.erase(&request);
586        assert!(result.success);
587        assert_eq!(result.records_erased, 3);
588        assert!(result.failed_scopes.is_empty());
589    }
590
591    #[test]
592    fn erasure_request_for_missing_subject() {
593        let sink = InMemoryErasureSink::new();
594        let request = ErasureRequest::new(
595            "req_002".into(),
596            "unknown_user".into(),
597            vec!["all".into()],
598        );
599
600        let result = sink.erase(&request);
601        assert!(result.success);
602        assert_eq!(result.records_erased, 0);
603    }
604
605    // --- New tests for M241 ---
606
607    #[test]
608    fn sensitive_display_masking_all_levels() {
609        let public = Sensitive::with_classification("data", ClassificationLevel::Public);
610        let internal = Sensitive::with_classification("data", ClassificationLevel::Internal);
611        let confidential =
612            Sensitive::with_classification("data", ClassificationLevel::Confidential);
613        let restricted = Sensitive::with_classification("data", ClassificationLevel::Restricted);
614
615        assert_eq!(format!("{}", public), "[REDACTED]");
616        assert_eq!(format!("{}", internal), "[REDACTED]");
617        assert_eq!(format!("{}", confidential), "[REDACTED]");
618        assert_eq!(format!("{}", restricted), "[REDACTED]");
619    }
620
621    #[test]
622    fn sensitive_debug_includes_level() {
623        let public = Sensitive::with_classification("data", ClassificationLevel::Public);
624        let restricted = Sensitive::with_classification("data", ClassificationLevel::Restricted);
625
626        assert_eq!(format!("{:?}", public), "[REDACTED:Public]");
627        assert_eq!(format!("{:?}", restricted), "[REDACTED:Restricted]");
628    }
629
630    #[test]
631    fn pii_detector_korean_resident_number() {
632        let detector = FieldNamePiiDetector::new();
633        assert_eq!(
634            detector.classify("jumin_number"),
635            Some(ClassificationLevel::Restricted)
636        );
637        assert_eq!(
638            detector.classify("resident_registration"),
639            Some(ClassificationLevel::Restricted)
640        );
641    }
642
643    #[test]
644    fn pii_detector_korean_business_number() {
645        let detector = FieldNamePiiDetector::new();
646        assert_eq!(
647            detector.classify("business_number"),
648            Some(ClassificationLevel::Confidential)
649        );
650        assert_eq!(
651            detector.classify("business_registration"),
652            Some(ClassificationLevel::Confidential)
653        );
654    }
655
656    #[test]
657    fn pii_detector_passport() {
658        let detector = FieldNamePiiDetector::new();
659        assert_eq!(
660            detector.classify("passport_number"),
661            Some(ClassificationLevel::Restricted)
662        );
663    }
664
665    #[test]
666    fn pii_detector_drivers_license() {
667        let detector = FieldNamePiiDetector::new();
668        assert_eq!(
669            detector.classify("drivers_license"),
670            Some(ClassificationLevel::Restricted)
671        );
672        assert_eq!(
673            detector.classify("license_number"),
674            Some(ClassificationLevel::Restricted)
675        );
676    }
677
678    #[test]
679    fn classification_level_serde_roundtrip() {
680        let level = ClassificationLevel::Restricted;
681        let json = serde_json::to_string(&level).unwrap();
682        let deser: ClassificationLevel = serde_json::from_str(&json).unwrap();
683        assert_eq!(deser, level);
684    }
685
686    #[test]
687    fn erasure_request_with_reason() {
688        let req = ErasureRequest::new("r1".into(), "user1".into(), vec!["all".into()])
689            .with_reason("GDPR Article 17");
690        assert_eq!(req.reason.as_deref(), Some("GDPR Article 17"));
691    }
692
693    #[test]
694    fn in_memory_erasure_sink_verify_post_erasure() {
695        let sink = InMemoryErasureSink::new();
696        sink.add_records("user_1", vec!["rec1".into(), "rec2".into()]);
697
698        let req = ErasureRequest::new("r1".into(), "user_1".into(), vec!["all".into()]);
699        let result = sink.erase(&req);
700        assert_eq!(result.records_erased, 2);
701
702        // Second erasure should find nothing
703        let result2 = sink.erase(&req);
704        assert_eq!(result2.records_erased, 0);
705    }
706
707    #[test]
708    fn pii_detector_total_categories() {
709        let detector = FieldNamePiiDetector::new();
710        // 9 original + 4 Korean = 13 categories
711        assert_eq!(detector.patterns.len(), 13);
712    }
713
714    #[test]
715    fn sensitive_into_inner_returns_value() {
716        let s = Sensitive::new(42);
717        assert_eq!(s.into_inner(), 42);
718    }
719
720    #[test]
721    fn xor_encryption_different_keys_differ() {
722        let hook1 = XorEncryption::new(0x42);
723        let hook2 = XorEncryption::new(0xFF);
724        let data = b"test";
725        assert_ne!(hook1.encrypt(data), hook2.encrypt(data));
726    }
727}