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)]
96#[allow(clippy::unwrap_used)]
97mod tests {
98    use super::*;
99
100    #[test]
101    fn test_privacy_evaluation_default() {
102        let eval = PrivacyEvaluation::default();
103        assert!(eval.passes);
104        assert!(eval.failures.is_empty());
105        assert!(eval.membership_inference.is_none());
106        assert!(eval.linkage.is_none());
107    }
108
109    #[test]
110    fn test_privacy_evaluation_with_failures() {
111        let mut eval = PrivacyEvaluation::new();
112
113        eval.membership_inference = Some(MiaResults {
114            auc_roc: 0.85,
115            accuracy: 0.80,
116            precision: 0.78,
117            recall: 0.82,
118            passes: false,
119            n_members: 100,
120            n_non_members: 100,
121            auc_threshold: 0.6,
122        });
123
124        eval.update_status();
125        assert!(!eval.passes);
126        assert_eq!(eval.failures.len(), 1);
127        assert!(eval.failures[0].contains("MIA"));
128    }
129
130    #[test]
131    fn test_privacy_evaluation_all_pass() {
132        let mut eval = PrivacyEvaluation::new();
133
134        eval.membership_inference = Some(MiaResults {
135            auc_roc: 0.52,
136            accuracy: 0.50,
137            precision: 0.50,
138            recall: 0.50,
139            passes: true,
140            n_members: 100,
141            n_non_members: 100,
142            auc_threshold: 0.6,
143        });
144
145        eval.linkage = Some(LinkageResults {
146            re_identification_rate: 0.01,
147            k_anonymity_achieved: 10,
148            unique_qi_combos_original: 50,
149            unique_qi_combos_synthetic: 48,
150            overlapping_combos: 30,
151            uniquely_linked: 1,
152            total_synthetic: 100,
153            passes: true,
154        });
155
156        eval.update_status();
157        assert!(eval.passes);
158        assert!(eval.failures.is_empty());
159    }
160
161    #[test]
162    fn test_privacy_evaluation_serde() {
163        let eval = PrivacyEvaluation::default();
164        let json = serde_json::to_string(&eval).unwrap();
165        let parsed: PrivacyEvaluation = serde_json::from_str(&json).unwrap();
166        assert!(parsed.passes);
167    }
168}