Skip to main content

datasynth_eval/privacy/
mod.rs

1//! Privacy evaluation module.
2//!
3//! Provides empirical privacy assessments for synthetic data including:
4//! - **Membership Inference Attack (MIA)**: Distance-based classifier to detect training data leakage
5//! - **Linkage Attack**: Quasi-identifier-based re-identification risk assessment
6//! - **NIST SP 800-226 Alignment**: Self-assessment against NIST criteria for de-identification
7//! - **SynQP Matrix**: Quality-privacy quadrant evaluation
8
9pub mod linkage;
10pub mod membership_inference;
11pub mod metrics;
12
13pub use linkage::{LinkageAttack, LinkageConfig, LinkageResults};
14pub use membership_inference::{MembershipInferenceAttack, MiaConfig, MiaResults};
15pub use metrics::{NistAlignmentReport, NistCriterion, SynQPMatrix, SynQPQuadrant};
16
17use serde::{Deserialize, Serialize};
18
19/// Combined privacy evaluation results.
20#[derive(Debug, Clone, Serialize, Deserialize)]
21pub struct PrivacyEvaluation {
22    /// Membership inference attack results (if evaluated).
23    #[serde(default, skip_serializing_if = "Option::is_none")]
24    pub membership_inference: Option<MiaResults>,
25    /// Linkage attack results (if evaluated).
26    #[serde(default, skip_serializing_if = "Option::is_none")]
27    pub linkage: Option<LinkageResults>,
28    /// NIST alignment report (if generated).
29    #[serde(default, skip_serializing_if = "Option::is_none")]
30    pub nist_alignment: Option<NistAlignmentReport>,
31    /// SynQP quality-privacy matrix (if computed).
32    #[serde(default, skip_serializing_if = "Option::is_none")]
33    pub synqp: Option<SynQPMatrix>,
34    /// Overall privacy evaluation passes.
35    pub passes: bool,
36    /// Failures encountered.
37    pub failures: Vec<String>,
38}
39
40impl Default for PrivacyEvaluation {
41    fn default() -> Self {
42        Self {
43            membership_inference: None,
44            linkage: None,
45            nist_alignment: None,
46            synqp: None,
47            passes: true,
48            failures: Vec::new(),
49        }
50    }
51}
52
53impl PrivacyEvaluation {
54    /// Create a new empty privacy evaluation.
55    pub fn new() -> Self {
56        Self::default()
57    }
58
59    /// Update the overall pass/fail status based on sub-evaluations.
60    pub fn update_status(&mut self) {
61        self.failures.clear();
62
63        if let Some(ref mia) = self.membership_inference {
64            if !mia.passes {
65                self.failures.push(format!(
66                    "MIA: AUC-ROC {:.4} exceeds threshold {:.4}",
67                    mia.auc_roc, mia.auc_threshold
68                ));
69            }
70        }
71
72        if let Some(ref linkage) = self.linkage {
73            if !linkage.passes {
74                self.failures.push(format!(
75                    "Linkage: re-identification rate {:.2}%, k-anonymity {}",
76                    linkage.re_identification_rate * 100.0,
77                    linkage.k_anonymity_achieved,
78                ));
79            }
80        }
81
82        if let Some(ref nist) = self.nist_alignment {
83            if !nist.passes {
84                self.failures.push(format!(
85                    "NIST alignment: score {:.0}% (requires >= 71%)",
86                    nist.alignment_score * 100.0
87                ));
88            }
89        }
90
91        self.passes = self.failures.is_empty();
92    }
93}
94
95#[cfg(test)]
96mod tests {
97    use super::*;
98
99    #[test]
100    fn test_privacy_evaluation_default() {
101        let eval = PrivacyEvaluation::default();
102        assert!(eval.passes);
103        assert!(eval.failures.is_empty());
104        assert!(eval.membership_inference.is_none());
105        assert!(eval.linkage.is_none());
106    }
107
108    #[test]
109    fn test_privacy_evaluation_with_failures() {
110        let mut eval = PrivacyEvaluation::new();
111
112        eval.membership_inference = Some(MiaResults {
113            auc_roc: 0.85,
114            accuracy: 0.80,
115            precision: 0.78,
116            recall: 0.82,
117            passes: false,
118            n_members: 100,
119            n_non_members: 100,
120            auc_threshold: 0.6,
121        });
122
123        eval.update_status();
124        assert!(!eval.passes);
125        assert_eq!(eval.failures.len(), 1);
126        assert!(eval.failures[0].contains("MIA"));
127    }
128
129    #[test]
130    fn test_privacy_evaluation_all_pass() {
131        let mut eval = PrivacyEvaluation::new();
132
133        eval.membership_inference = Some(MiaResults {
134            auc_roc: 0.52,
135            accuracy: 0.50,
136            precision: 0.50,
137            recall: 0.50,
138            passes: true,
139            n_members: 100,
140            n_non_members: 100,
141            auc_threshold: 0.6,
142        });
143
144        eval.linkage = Some(LinkageResults {
145            re_identification_rate: 0.01,
146            k_anonymity_achieved: 10,
147            unique_qi_combos_original: 50,
148            unique_qi_combos_synthetic: 48,
149            overlapping_combos: 30,
150            uniquely_linked: 1,
151            total_synthetic: 100,
152            passes: true,
153        });
154
155        eval.update_status();
156        assert!(eval.passes);
157        assert!(eval.failures.is_empty());
158    }
159
160    #[test]
161    fn test_privacy_evaluation_serde() {
162        let eval = PrivacyEvaluation::default();
163        let json = serde_json::to_string(&eval).unwrap();
164        let parsed: PrivacyEvaluation = serde_json::from_str(&json).unwrap();
165        assert!(parsed.passes);
166    }
167}