use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NistAlignmentReport {
pub differential_privacy_applied: bool,
pub epsilon: Option<f64>,
pub delta: Option<f64>,
pub composition_method: Option<String>,
pub k_anonymity_enforced: bool,
pub k_anonymity_level: Option<usize>,
pub membership_inference_tested: bool,
pub mia_auc_roc: Option<f64>,
pub linkage_attack_tested: bool,
pub re_identification_rate: Option<f64>,
pub alignment_score: f64,
pub criteria: Vec<NistCriterion>,
pub passes: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NistCriterion {
pub id: String,
pub description: String,
pub met: bool,
pub evidence: String,
}
impl NistAlignmentReport {
pub fn build(
dp_applied: bool,
epsilon: Option<f64>,
delta: Option<f64>,
composition_method: Option<String>,
k_anonymity_enforced: bool,
k_anonymity_level: Option<usize>,
mia_auc_roc: Option<f64>,
re_identification_rate: Option<f64>,
) -> Self {
let mut criteria = Vec::new();
criteria.push(NistCriterion {
id: "DP-1".to_string(),
description: "Differential privacy mechanism applied".to_string(),
met: dp_applied,
evidence: if dp_applied {
format!(
"DP applied with epsilon={}, delta={}, method={}",
epsilon.map_or("N/A".to_string(), |e| format!("{e:.4}")),
delta.map_or("N/A".to_string(), |d| format!("{d:.2e}")),
composition_method.as_deref().unwrap_or("naive"),
)
} else {
"No differential privacy mechanism applied".to_string()
},
});
criteria.push(NistCriterion {
id: "DP-2".to_string(),
description: "Epsilon within reasonable bounds (< 10.0)".to_string(),
met: epsilon.is_some_and(|e| e < 10.0),
evidence: epsilon.map_or("No epsilon specified".to_string(), |e| {
format!("Epsilon = {e:.4}")
}),
});
criteria.push(NistCriterion {
id: "KA-1".to_string(),
description: "K-anonymity enforced with k >= 5".to_string(),
met: k_anonymity_enforced && k_anonymity_level.is_some_and(|k| k >= 5),
evidence: if k_anonymity_enforced {
format!(
"K-anonymity enforced, k = {}",
k_anonymity_level.map_or("unknown".to_string(), |k| k.to_string())
)
} else {
"K-anonymity not enforced".to_string()
},
});
let mia_tested = mia_auc_roc.is_some();
criteria.push(NistCriterion {
id: "MIA-1".to_string(),
description: "Membership inference attack tested".to_string(),
met: mia_tested,
evidence: if mia_tested {
format!("MIA AUC-ROC = {:.4}", mia_auc_roc.unwrap_or(0.0))
} else {
"MIA not tested".to_string()
},
});
criteria.push(NistCriterion {
id: "MIA-2".to_string(),
description: "MIA AUC-ROC < 0.6 (near-random)".to_string(),
met: mia_auc_roc.is_some_and(|auc| auc < 0.6),
evidence: mia_auc_roc.map_or("MIA not tested".to_string(), |auc| {
format!("AUC-ROC = {auc:.4}")
}),
});
let linkage_tested = re_identification_rate.is_some();
criteria.push(NistCriterion {
id: "LA-1".to_string(),
description: "Linkage attack tested".to_string(),
met: linkage_tested,
evidence: if linkage_tested {
format!(
"Re-identification rate = {:.4}",
re_identification_rate.unwrap_or(0.0)
)
} else {
"Linkage attack not tested".to_string()
},
});
criteria.push(NistCriterion {
id: "LA-2".to_string(),
description: "Re-identification rate < 5%".to_string(),
met: re_identification_rate.is_some_and(|r| r < 0.05),
evidence: re_identification_rate.map_or("Not tested".to_string(), |r| {
format!("Re-identification rate = {:.2}%", r * 100.0)
}),
});
let met_count = criteria.iter().filter(|c| c.met).count();
let alignment_score = if criteria.is_empty() {
0.0
} else {
met_count as f64 / criteria.len() as f64
};
let passes = met_count >= 5;
Self {
differential_privacy_applied: dp_applied,
epsilon,
delta,
composition_method,
k_anonymity_enforced,
k_anonymity_level,
membership_inference_tested: mia_tested,
mia_auc_roc,
linkage_attack_tested: linkage_tested,
re_identification_rate,
alignment_score,
criteria,
passes,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum SynQPQuadrant {
HighQHighP,
HighQLowP,
LowQHighP,
LowQLowP,
}
impl std::fmt::Display for SynQPQuadrant {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::HighQHighP => write!(f, "High Quality / High Privacy (Ideal)"),
Self::HighQLowP => write!(f, "High Quality / Low Privacy (Risky)"),
Self::LowQHighP => write!(f, "Low Quality / High Privacy (Conservative)"),
Self::LowQLowP => write!(f, "Low Quality / Low Privacy (Poor)"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SynQPMatrix {
pub quality_score: f64,
pub privacy_score: f64,
pub quadrant: SynQPQuadrant,
pub quality_threshold: f64,
pub privacy_threshold: f64,
}
impl SynQPMatrix {
pub fn evaluate(
quality_score: f64,
privacy_score: f64,
quality_threshold: f64,
privacy_threshold: f64,
) -> Self {
let quadrant = match (
quality_score >= quality_threshold,
privacy_score >= privacy_threshold,
) {
(true, true) => SynQPQuadrant::HighQHighP,
(true, false) => SynQPQuadrant::HighQLowP,
(false, true) => SynQPQuadrant::LowQHighP,
(false, false) => SynQPQuadrant::LowQLowP,
};
Self {
quality_score,
privacy_score,
quadrant,
quality_threshold,
privacy_threshold,
}
}
pub fn evaluate_default(quality_score: f64, privacy_score: f64) -> Self {
Self::evaluate(quality_score, privacy_score, 0.7, 0.7)
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
#[test]
fn test_nist_report_all_criteria_met() {
let report = NistAlignmentReport::build(
true,
Some(1.0),
Some(1e-5),
Some("renyi_dp".to_string()),
true,
Some(10),
Some(0.52),
Some(0.01),
);
assert!(report.passes);
assert!(report.alignment_score > 0.9);
assert_eq!(report.criteria.len(), 7);
assert!(report.criteria.iter().all(|c| c.met));
}
#[test]
fn test_nist_report_no_privacy() {
let report = NistAlignmentReport::build(
false, None, None, None, false, None, None, None, );
assert!(!report.passes);
assert_eq!(report.alignment_score, 0.0);
assert!(report.criteria.iter().all(|c| !c.met));
}
#[test]
fn test_nist_report_partial() {
let report = NistAlignmentReport::build(
true,
Some(5.0),
Some(1e-5),
Some("naive".to_string()),
true,
Some(3), Some(0.55), Some(0.03), );
let met = report.criteria.iter().filter(|c| c.met).count();
assert_eq!(met, 6); assert!(report.passes);
}
#[test]
fn test_nist_report_serde() {
let report = NistAlignmentReport::build(
true,
Some(1.0),
Some(1e-5),
None,
true,
Some(10),
Some(0.5),
Some(0.01),
);
let json = serde_json::to_string(&report).unwrap();
let parsed: NistAlignmentReport = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.criteria.len(), 7);
assert!(parsed.passes);
}
#[test]
fn test_synqp_high_quality_high_privacy() {
let matrix = SynQPMatrix::evaluate_default(0.85, 0.90);
assert_eq!(matrix.quadrant, SynQPQuadrant::HighQHighP);
}
#[test]
fn test_synqp_high_quality_low_privacy() {
let matrix = SynQPMatrix::evaluate_default(0.85, 0.40);
assert_eq!(matrix.quadrant, SynQPQuadrant::HighQLowP);
}
#[test]
fn test_synqp_low_quality_high_privacy() {
let matrix = SynQPMatrix::evaluate_default(0.30, 0.90);
assert_eq!(matrix.quadrant, SynQPQuadrant::LowQHighP);
}
#[test]
fn test_synqp_low_quality_low_privacy() {
let matrix = SynQPMatrix::evaluate_default(0.30, 0.40);
assert_eq!(matrix.quadrant, SynQPQuadrant::LowQLowP);
}
#[test]
fn test_synqp_custom_thresholds() {
let matrix = SynQPMatrix::evaluate(0.5, 0.5, 0.3, 0.3);
assert_eq!(matrix.quadrant, SynQPQuadrant::HighQHighP);
}
#[test]
fn test_synqp_display() {
assert_eq!(
format!("{}", SynQPQuadrant::HighQHighP),
"High Quality / High Privacy (Ideal)"
);
assert_eq!(
format!("{}", SynQPQuadrant::LowQLowP),
"Low Quality / Low Privacy (Poor)"
);
}
#[test]
fn test_synqp_serde() {
let matrix = SynQPMatrix::evaluate_default(0.8, 0.9);
let json = serde_json::to_string(&matrix).unwrap();
let parsed: SynQPMatrix = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.quadrant, SynQPQuadrant::HighQHighP);
assert!((parsed.quality_score - 0.8).abs() < 1e-10);
}
}