1use serde::{Deserialize, Serialize};
7
8#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct NistAlignmentReport {
14 pub differential_privacy_applied: bool,
16 pub epsilon: Option<f64>,
18 pub delta: Option<f64>,
20 pub composition_method: Option<String>,
22 pub k_anonymity_enforced: bool,
24 pub k_anonymity_level: Option<usize>,
26 pub membership_inference_tested: bool,
28 pub mia_auc_roc: Option<f64>,
30 pub linkage_attack_tested: bool,
32 pub re_identification_rate: Option<f64>,
34 pub alignment_score: f64,
37 pub criteria: Vec<NistCriterion>,
39 pub passes: bool,
41}
42
43#[derive(Debug, Clone, Serialize, Deserialize)]
45pub struct NistCriterion {
46 pub id: String,
48 pub description: String,
50 pub met: bool,
52 pub evidence: String,
54}
55
56impl NistAlignmentReport {
57 pub fn build(
59 dp_applied: bool,
60 epsilon: Option<f64>,
61 delta: Option<f64>,
62 composition_method: Option<String>,
63 k_anonymity_enforced: bool,
64 k_anonymity_level: Option<usize>,
65 mia_auc_roc: Option<f64>,
66 re_identification_rate: Option<f64>,
67 ) -> Self {
68 let mut criteria = Vec::new();
69
70 criteria.push(NistCriterion {
72 id: "DP-1".to_string(),
73 description: "Differential privacy mechanism applied".to_string(),
74 met: dp_applied,
75 evidence: if dp_applied {
76 format!(
77 "DP applied with epsilon={}, delta={}, method={}",
78 epsilon.map_or("N/A".to_string(), |e| format!("{:.4}", e)),
79 delta.map_or("N/A".to_string(), |d| format!("{:.2e}", d)),
80 composition_method.as_deref().unwrap_or("naive"),
81 )
82 } else {
83 "No differential privacy mechanism applied".to_string()
84 },
85 });
86
87 criteria.push(NistCriterion {
88 id: "DP-2".to_string(),
89 description: "Epsilon within reasonable bounds (< 10.0)".to_string(),
90 met: epsilon.is_some_and(|e| e < 10.0),
91 evidence: epsilon.map_or("No epsilon specified".to_string(), |e| {
92 format!("Epsilon = {:.4}", e)
93 }),
94 });
95
96 criteria.push(NistCriterion {
98 id: "KA-1".to_string(),
99 description: "K-anonymity enforced with k >= 5".to_string(),
100 met: k_anonymity_enforced && k_anonymity_level.is_some_and(|k| k >= 5),
101 evidence: if k_anonymity_enforced {
102 format!(
103 "K-anonymity enforced, k = {}",
104 k_anonymity_level.map_or("unknown".to_string(), |k| k.to_string())
105 )
106 } else {
107 "K-anonymity not enforced".to_string()
108 },
109 });
110
111 let mia_tested = mia_auc_roc.is_some();
113 criteria.push(NistCriterion {
114 id: "MIA-1".to_string(),
115 description: "Membership inference attack tested".to_string(),
116 met: mia_tested,
117 evidence: if mia_tested {
118 format!("MIA AUC-ROC = {:.4}", mia_auc_roc.unwrap_or(0.0))
119 } else {
120 "MIA not tested".to_string()
121 },
122 });
123
124 criteria.push(NistCriterion {
125 id: "MIA-2".to_string(),
126 description: "MIA AUC-ROC < 0.6 (near-random)".to_string(),
127 met: mia_auc_roc.is_some_and(|auc| auc < 0.6),
128 evidence: mia_auc_roc.map_or("MIA not tested".to_string(), |auc| {
129 format!("AUC-ROC = {:.4}", auc)
130 }),
131 });
132
133 let linkage_tested = re_identification_rate.is_some();
135 criteria.push(NistCriterion {
136 id: "LA-1".to_string(),
137 description: "Linkage attack tested".to_string(),
138 met: linkage_tested,
139 evidence: if linkage_tested {
140 format!(
141 "Re-identification rate = {:.4}",
142 re_identification_rate.unwrap_or(0.0)
143 )
144 } else {
145 "Linkage attack not tested".to_string()
146 },
147 });
148
149 criteria.push(NistCriterion {
150 id: "LA-2".to_string(),
151 description: "Re-identification rate < 5%".to_string(),
152 met: re_identification_rate.is_some_and(|r| r < 0.05),
153 evidence: re_identification_rate.map_or("Not tested".to_string(), |r| {
154 format!("Re-identification rate = {:.2}%", r * 100.0)
155 }),
156 });
157
158 let met_count = criteria.iter().filter(|c| c.met).count();
159 let alignment_score = if criteria.is_empty() {
160 0.0
161 } else {
162 met_count as f64 / criteria.len() as f64
163 };
164
165 let passes = met_count >= 5;
167
168 Self {
169 differential_privacy_applied: dp_applied,
170 epsilon,
171 delta,
172 composition_method,
173 k_anonymity_enforced,
174 k_anonymity_level,
175 membership_inference_tested: mia_tested,
176 mia_auc_roc,
177 linkage_attack_tested: linkage_tested,
178 re_identification_rate,
179 alignment_score,
180 criteria,
181 passes,
182 }
183 }
184}
185
186#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
191pub enum SynQPQuadrant {
192 HighQHighP,
194 HighQLowP,
196 LowQHighP,
198 LowQLowP,
200}
201
202impl std::fmt::Display for SynQPQuadrant {
203 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
204 match self {
205 Self::HighQHighP => write!(f, "High Quality / High Privacy (Ideal)"),
206 Self::HighQLowP => write!(f, "High Quality / Low Privacy (Risky)"),
207 Self::LowQHighP => write!(f, "Low Quality / High Privacy (Conservative)"),
208 Self::LowQLowP => write!(f, "Low Quality / Low Privacy (Poor)"),
209 }
210 }
211}
212
213#[derive(Debug, Clone, Serialize, Deserialize)]
215pub struct SynQPMatrix {
216 pub quality_score: f64,
218 pub privacy_score: f64,
220 pub quadrant: SynQPQuadrant,
222 pub quality_threshold: f64,
224 pub privacy_threshold: f64,
226}
227
228impl SynQPMatrix {
229 pub fn evaluate(
237 quality_score: f64,
238 privacy_score: f64,
239 quality_threshold: f64,
240 privacy_threshold: f64,
241 ) -> Self {
242 let quadrant = match (
243 quality_score >= quality_threshold,
244 privacy_score >= privacy_threshold,
245 ) {
246 (true, true) => SynQPQuadrant::HighQHighP,
247 (true, false) => SynQPQuadrant::HighQLowP,
248 (false, true) => SynQPQuadrant::LowQHighP,
249 (false, false) => SynQPQuadrant::LowQLowP,
250 };
251
252 Self {
253 quality_score,
254 privacy_score,
255 quadrant,
256 quality_threshold,
257 privacy_threshold,
258 }
259 }
260
261 pub fn evaluate_default(quality_score: f64, privacy_score: f64) -> Self {
263 Self::evaluate(quality_score, privacy_score, 0.7, 0.7)
264 }
265}
266
267#[cfg(test)]
268#[allow(clippy::unwrap_used)]
269mod tests {
270 use super::*;
271
272 #[test]
273 fn test_nist_report_all_criteria_met() {
274 let report = NistAlignmentReport::build(
275 true,
276 Some(1.0),
277 Some(1e-5),
278 Some("renyi_dp".to_string()),
279 true,
280 Some(10),
281 Some(0.52),
282 Some(0.01),
283 );
284
285 assert!(report.passes);
286 assert!(report.alignment_score > 0.9);
287 assert_eq!(report.criteria.len(), 7);
288 assert!(report.criteria.iter().all(|c| c.met));
289 }
290
291 #[test]
292 fn test_nist_report_no_privacy() {
293 let report = NistAlignmentReport::build(
294 false, None, None, None, false, None, None, None, );
299
300 assert!(!report.passes);
301 assert_eq!(report.alignment_score, 0.0);
302 assert!(report.criteria.iter().all(|c| !c.met));
303 }
304
305 #[test]
306 fn test_nist_report_partial() {
307 let report = NistAlignmentReport::build(
308 true,
309 Some(5.0),
310 Some(1e-5),
311 Some("naive".to_string()),
312 true,
313 Some(3), Some(0.55), Some(0.03), );
317
318 let met = report.criteria.iter().filter(|c| c.met).count();
321 assert_eq!(met, 6); assert!(report.passes);
323 }
324
325 #[test]
326 fn test_nist_report_serde() {
327 let report = NistAlignmentReport::build(
328 true,
329 Some(1.0),
330 Some(1e-5),
331 None,
332 true,
333 Some(10),
334 Some(0.5),
335 Some(0.01),
336 );
337 let json = serde_json::to_string(&report).unwrap();
338 let parsed: NistAlignmentReport = serde_json::from_str(&json).unwrap();
339 assert_eq!(parsed.criteria.len(), 7);
340 assert!(parsed.passes);
341 }
342
343 #[test]
344 fn test_synqp_high_quality_high_privacy() {
345 let matrix = SynQPMatrix::evaluate_default(0.85, 0.90);
346 assert_eq!(matrix.quadrant, SynQPQuadrant::HighQHighP);
347 }
348
349 #[test]
350 fn test_synqp_high_quality_low_privacy() {
351 let matrix = SynQPMatrix::evaluate_default(0.85, 0.40);
352 assert_eq!(matrix.quadrant, SynQPQuadrant::HighQLowP);
353 }
354
355 #[test]
356 fn test_synqp_low_quality_high_privacy() {
357 let matrix = SynQPMatrix::evaluate_default(0.30, 0.90);
358 assert_eq!(matrix.quadrant, SynQPQuadrant::LowQHighP);
359 }
360
361 #[test]
362 fn test_synqp_low_quality_low_privacy() {
363 let matrix = SynQPMatrix::evaluate_default(0.30, 0.40);
364 assert_eq!(matrix.quadrant, SynQPQuadrant::LowQLowP);
365 }
366
367 #[test]
368 fn test_synqp_custom_thresholds() {
369 let matrix = SynQPMatrix::evaluate(0.5, 0.5, 0.3, 0.3);
371 assert_eq!(matrix.quadrant, SynQPQuadrant::HighQHighP);
372 }
373
374 #[test]
375 fn test_synqp_display() {
376 assert_eq!(
377 format!("{}", SynQPQuadrant::HighQHighP),
378 "High Quality / High Privacy (Ideal)"
379 );
380 assert_eq!(
381 format!("{}", SynQPQuadrant::LowQLowP),
382 "Low Quality / Low Privacy (Poor)"
383 );
384 }
385
386 #[test]
387 fn test_synqp_serde() {
388 let matrix = SynQPMatrix::evaluate_default(0.8, 0.9);
389 let json = serde_json::to_string(&matrix).unwrap();
390 let parsed: SynQPMatrix = serde_json::from_str(&json).unwrap();
391 assert_eq!(parsed.quadrant, SynQPQuadrant::HighQHighP);
392 assert!((parsed.quality_score - 0.8).abs() < 1e-10);
393 }
394}