use serde::{Deserialize, Serialize};
use std::fmt;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum DataRegion {
EU,
US,
LATAM,
APAC,
Custom(String),
}
impl fmt::Display for DataRegion {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
DataRegion::EU => write!(f, "EU"),
DataRegion::US => write!(f, "US"),
DataRegion::LATAM => write!(f, "LATAM"),
DataRegion::APAC => write!(f, "APAC"),
DataRegion::Custom(s) => write!(f, "Custom({s})"),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum LlmRoutingPolicy {
AnyRegion,
SameRegion,
PreferRegion,
ExplicitEndpoints(Vec<String>),
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum PiiHandlingPolicy {
Redact,
Encrypt,
Allow,
Reject,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DataResidencyConfig {
pub region: DataRegion,
pub data_storage_location: String,
pub llm_routing_policy: LlmRoutingPolicy,
pub encryption_at_rest: bool,
pub encryption_in_transit: bool,
pub data_retention_days: u64,
pub pii_handling: PiiHandlingPolicy,
pub cross_border_transfer: bool,
pub audit_data_location: String,
}
impl Default for DataResidencyConfig {
fn default() -> Self {
Self {
region: DataRegion::US,
data_storage_location: "/var/lib/argentor/data".to_string(),
llm_routing_policy: LlmRoutingPolicy::AnyRegion,
encryption_at_rest: true,
encryption_in_transit: true,
data_retention_days: 90,
pii_handling: PiiHandlingPolicy::Redact,
cross_border_transfer: false,
audit_data_location: "/var/lib/argentor/audit".to_string(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum IssueSeverity {
Error,
Warning,
Info,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ResidencyIssue {
pub severity: IssueSeverity,
pub field: String,
pub message: String,
}
impl fmt::Display for ResidencyIssue {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let level = match self.severity {
IssueSeverity::Error => "ERROR",
IssueSeverity::Warning => "WARN",
IssueSeverity::Info => "INFO",
};
write!(f, "[{}] {}: {}", level, self.field, self.message)
}
}
pub struct ResidencyValidator;
impl ResidencyValidator {
pub fn validate_config(config: &DataResidencyConfig) -> Vec<ResidencyIssue> {
let mut issues = Vec::new();
if config.data_storage_location.trim().is_empty() {
issues.push(ResidencyIssue {
severity: IssueSeverity::Error,
field: "data_storage_location".into(),
message: "Data storage location must not be empty".into(),
});
}
if config.audit_data_location.trim().is_empty() {
issues.push(ResidencyIssue {
severity: IssueSeverity::Error,
field: "audit_data_location".into(),
message: "Audit data location must not be empty".into(),
});
}
if !config.encryption_at_rest {
let sev = match config.region {
DataRegion::EU | DataRegion::LATAM => IssueSeverity::Error,
_ => IssueSeverity::Warning,
};
issues.push(ResidencyIssue {
severity: sev,
field: "encryption_at_rest".into(),
message:
"Encryption at rest is disabled — this may violate compliance requirements"
.into(),
});
}
if !config.encryption_in_transit {
issues.push(ResidencyIssue {
severity: IssueSeverity::Error,
field: "encryption_in_transit".into(),
message: "Encryption in transit (TLS) must be enabled for production deployments"
.into(),
});
}
if config.region == DataRegion::EU {
match &config.llm_routing_policy {
LlmRoutingPolicy::AnyRegion => {
issues.push(ResidencyIssue {
severity: IssueSeverity::Error,
field: "llm_routing_policy".into(),
message: "EU region requires SameRegion or ExplicitEndpoints routing for GDPR compliance".into(),
});
}
LlmRoutingPolicy::PreferRegion => {
issues.push(ResidencyIssue {
severity: IssueSeverity::Warning,
field: "llm_routing_policy".into(),
message: "PreferRegion may route outside EU under load — consider SameRegion for strict GDPR".into(),
});
}
_ => {}
}
}
if config.region == DataRegion::EU && config.cross_border_transfer {
issues.push(ResidencyIssue {
severity: IssueSeverity::Warning,
field: "cross_border_transfer".into(),
message: "Cross-border transfer enabled for EU — ensure adequate safeguards (SCCs, adequacy decisions)".into(),
});
}
if config.pii_handling == PiiHandlingPolicy::Allow {
issues.push(ResidencyIssue {
severity: IssueSeverity::Warning,
field: "pii_handling".into(),
message:
"PII handling set to Allow — only appropriate for fully on-premises deployments"
.into(),
});
}
if config.data_retention_days < 7 {
issues.push(ResidencyIssue {
severity: IssueSeverity::Warning,
field: "data_retention_days".into(),
message: "Data retention under 7 days may be insufficient for audit requirements"
.into(),
});
}
if config.data_retention_days == 0 {
issues.push(ResidencyIssue {
severity: IssueSeverity::Error,
field: "data_retention_days".into(),
message: "Data retention of 0 days means data is purged immediately — likely a misconfiguration".into(),
});
}
if let LlmRoutingPolicy::ExplicitEndpoints(ref eps) = config.llm_routing_policy {
if eps.is_empty() {
issues.push(ResidencyIssue {
severity: IssueSeverity::Error,
field: "llm_routing_policy".into(),
message:
"ExplicitEndpoints list is empty — at least one endpoint URL is required"
.into(),
});
}
}
issues
}
pub fn is_compliant(config: &DataResidencyConfig, framework: &str) -> bool {
match framework.to_lowercase().as_str() {
"gdpr" => {
config.encryption_at_rest
&& config.encryption_in_transit
&& matches!(
config.llm_routing_policy,
LlmRoutingPolicy::SameRegion | LlmRoutingPolicy::ExplicitEndpoints(_)
)
&& matches!(
config.pii_handling,
PiiHandlingPolicy::Redact
| PiiHandlingPolicy::Encrypt
| PiiHandlingPolicy::Reject
)
&& (config.region == DataRegion::EU || !config.cross_border_transfer)
}
"hipaa" => {
config.encryption_at_rest
&& config.encryption_in_transit
&& !config.cross_border_transfer
&& matches!(
config.pii_handling,
PiiHandlingPolicy::Encrypt | PiiHandlingPolicy::Reject
)
&& config.data_retention_days >= 2555 }
"iso27001" => {
config.encryption_at_rest
&& config.encryption_in_transit
&& config.data_retention_days >= 30
&& config.pii_handling != PiiHandlingPolicy::Allow
}
"iso42001" => {
config.encryption_at_rest
&& config.encryption_in_transit
&& config.pii_handling != PiiHandlingPolicy::Allow
&& !matches!(config.llm_routing_policy, LlmRoutingPolicy::AnyRegion)
}
"dpga" => {
config.encryption_at_rest
&& config.encryption_in_transit
&& config.pii_handling != PiiHandlingPolicy::Allow
}
"sox" => {
config.encryption_at_rest
&& config.encryption_in_transit
&& config.data_retention_days >= 2555
&& !config.audit_data_location.trim().is_empty()
}
_ => false,
}
}
pub fn suggest_config(
region: DataRegion,
compliance_frameworks: &[&str],
) -> DataResidencyConfig {
let needs_gdpr = compliance_frameworks
.iter()
.any(|f| f.eq_ignore_ascii_case("gdpr"));
let needs_hipaa = compliance_frameworks
.iter()
.any(|f| f.eq_ignore_ascii_case("hipaa"));
let needs_sox = compliance_frameworks
.iter()
.any(|f| f.eq_ignore_ascii_case("sox"));
let region_label = match ®ion.clone() {
DataRegion::EU => "eu".to_string(),
DataRegion::US => "us".to_string(),
DataRegion::LATAM => "latam".to_string(),
DataRegion::APAC => "apac".to_string(),
DataRegion::Custom(s) => s.clone(),
};
let routing = if needs_gdpr || region == DataRegion::EU {
LlmRoutingPolicy::SameRegion
} else {
LlmRoutingPolicy::PreferRegion
};
let pii = if needs_hipaa {
PiiHandlingPolicy::Encrypt
} else {
PiiHandlingPolicy::Redact
};
let retention = if needs_hipaa || needs_sox {
2555 } else if needs_gdpr {
30
} else {
90
};
let cross_border = !(needs_gdpr || needs_hipaa);
DataResidencyConfig {
region,
data_storage_location: format!("/var/lib/argentor/data/{region_label}"),
llm_routing_policy: routing,
encryption_at_rest: true,
encryption_in_transit: true,
data_retention_days: retention,
pii_handling: pii,
cross_border_transfer: cross_border,
audit_data_location: format!("/var/lib/argentor/audit/{region_label}"),
}
}
}
pub fn eu_gdpr_config() -> DataResidencyConfig {
DataResidencyConfig {
region: DataRegion::EU,
data_storage_location: "/var/lib/argentor/data/eu".to_string(),
llm_routing_policy: LlmRoutingPolicy::SameRegion,
encryption_at_rest: true,
encryption_in_transit: true,
data_retention_days: 30,
pii_handling: PiiHandlingPolicy::Redact,
cross_border_transfer: false,
audit_data_location: "/var/lib/argentor/audit/eu".to_string(),
}
}
pub fn us_standard_config() -> DataResidencyConfig {
DataResidencyConfig {
region: DataRegion::US,
data_storage_location: "/var/lib/argentor/data/us".to_string(),
llm_routing_policy: LlmRoutingPolicy::PreferRegion,
encryption_at_rest: true,
encryption_in_transit: true,
data_retention_days: 90,
pii_handling: PiiHandlingPolicy::Redact,
cross_border_transfer: false,
audit_data_location: "/var/lib/argentor/audit/us".to_string(),
}
}
pub fn latam_config() -> DataResidencyConfig {
DataResidencyConfig {
region: DataRegion::LATAM,
data_storage_location: "/var/lib/argentor/data/latam".to_string(),
llm_routing_policy: LlmRoutingPolicy::PreferRegion,
encryption_at_rest: true,
encryption_in_transit: true,
data_retention_days: 90,
pii_handling: PiiHandlingPolicy::Redact,
cross_border_transfer: false,
audit_data_location: "/var/lib/argentor/audit/latam".to_string(),
}
}
pub fn hipaa_config() -> DataResidencyConfig {
DataResidencyConfig {
region: DataRegion::US,
data_storage_location: "/var/lib/argentor/data/us-hipaa".to_string(),
llm_routing_policy: LlmRoutingPolicy::SameRegion,
encryption_at_rest: true,
encryption_in_transit: true,
data_retention_days: 2555,
pii_handling: PiiHandlingPolicy::Encrypt,
cross_border_transfer: false,
audit_data_location: "/var/lib/argentor/audit/us-hipaa".to_string(),
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
#[test]
fn test_eu_gdpr_config_defaults() {
let cfg = eu_gdpr_config();
assert_eq!(cfg.region, DataRegion::EU);
assert!(cfg.encryption_at_rest);
assert!(cfg.encryption_in_transit);
assert_eq!(cfg.data_retention_days, 30);
assert_eq!(cfg.pii_handling, PiiHandlingPolicy::Redact);
assert!(!cfg.cross_border_transfer);
assert_eq!(cfg.llm_routing_policy, LlmRoutingPolicy::SameRegion);
}
#[test]
fn test_us_standard_config_defaults() {
let cfg = us_standard_config();
assert_eq!(cfg.region, DataRegion::US);
assert_eq!(cfg.data_retention_days, 90);
assert_eq!(cfg.llm_routing_policy, LlmRoutingPolicy::PreferRegion);
}
#[test]
fn test_latam_config_defaults() {
let cfg = latam_config();
assert_eq!(cfg.region, DataRegion::LATAM);
assert!(cfg.encryption_at_rest);
assert!(!cfg.cross_border_transfer);
}
#[test]
fn test_hipaa_config_defaults() {
let cfg = hipaa_config();
assert_eq!(cfg.region, DataRegion::US);
assert_eq!(cfg.pii_handling, PiiHandlingPolicy::Encrypt);
assert_eq!(cfg.data_retention_days, 2555);
assert!(!cfg.cross_border_transfer);
assert_eq!(cfg.llm_routing_policy, LlmRoutingPolicy::SameRegion);
}
#[test]
fn test_valid_eu_config_has_no_issues() {
let cfg = eu_gdpr_config();
let issues = ResidencyValidator::validate_config(&cfg);
assert!(issues.is_empty(), "Expected no issues, got: {issues:?}");
}
#[test]
fn test_valid_hipaa_config_has_no_issues() {
let cfg = hipaa_config();
let issues = ResidencyValidator::validate_config(&cfg);
assert!(issues.is_empty(), "Expected no issues, got: {issues:?}");
}
#[test]
fn test_empty_storage_location_is_error() {
let mut cfg = us_standard_config();
cfg.data_storage_location = "".to_string();
let issues = ResidencyValidator::validate_config(&cfg);
assert!(issues
.iter()
.any(|i| i.field == "data_storage_location" && i.severity == IssueSeverity::Error));
}
#[test]
fn test_empty_audit_location_is_error() {
let mut cfg = us_standard_config();
cfg.audit_data_location = " ".to_string();
let issues = ResidencyValidator::validate_config(&cfg);
assert!(issues
.iter()
.any(|i| i.field == "audit_data_location" && i.severity == IssueSeverity::Error));
}
#[test]
fn test_eu_anyregion_routing_is_error() {
let mut cfg = eu_gdpr_config();
cfg.llm_routing_policy = LlmRoutingPolicy::AnyRegion;
let issues = ResidencyValidator::validate_config(&cfg);
assert!(issues
.iter()
.any(|i| i.field == "llm_routing_policy" && i.severity == IssueSeverity::Error));
}
#[test]
fn test_eu_prefer_region_is_warning() {
let mut cfg = eu_gdpr_config();
cfg.llm_routing_policy = LlmRoutingPolicy::PreferRegion;
let issues = ResidencyValidator::validate_config(&cfg);
assert!(issues
.iter()
.any(|i| i.field == "llm_routing_policy" && i.severity == IssueSeverity::Warning));
}
#[test]
fn test_eu_cross_border_warning() {
let mut cfg = eu_gdpr_config();
cfg.cross_border_transfer = true;
let issues = ResidencyValidator::validate_config(&cfg);
assert!(issues
.iter()
.any(|i| i.field == "cross_border_transfer" && i.severity == IssueSeverity::Warning));
}
#[test]
fn test_pii_allow_is_warning() {
let mut cfg = us_standard_config();
cfg.pii_handling = PiiHandlingPolicy::Allow;
let issues = ResidencyValidator::validate_config(&cfg);
assert!(issues
.iter()
.any(|i| i.field == "pii_handling" && i.severity == IssueSeverity::Warning));
}
#[test]
fn test_zero_retention_is_error() {
let mut cfg = us_standard_config();
cfg.data_retention_days = 0;
let issues = ResidencyValidator::validate_config(&cfg);
assert!(issues
.iter()
.any(|i| i.field == "data_retention_days" && i.severity == IssueSeverity::Error));
}
#[test]
fn test_short_retention_is_warning() {
let mut cfg = us_standard_config();
cfg.data_retention_days = 3;
let issues = ResidencyValidator::validate_config(&cfg);
assert!(issues
.iter()
.any(|i| i.field == "data_retention_days" && i.severity == IssueSeverity::Warning));
}
#[test]
fn test_empty_explicit_endpoints_is_error() {
let mut cfg = us_standard_config();
cfg.llm_routing_policy = LlmRoutingPolicy::ExplicitEndpoints(vec![]);
let issues = ResidencyValidator::validate_config(&cfg);
assert!(issues
.iter()
.any(|i| i.field == "llm_routing_policy" && i.severity == IssueSeverity::Error));
}
#[test]
fn test_encryption_disabled_eu_is_error() {
let mut cfg = eu_gdpr_config();
cfg.encryption_at_rest = false;
let issues = ResidencyValidator::validate_config(&cfg);
assert!(issues
.iter()
.any(|i| i.field == "encryption_at_rest" && i.severity == IssueSeverity::Error));
}
#[test]
fn test_encryption_disabled_us_is_warning() {
let mut cfg = us_standard_config();
cfg.encryption_at_rest = false;
let issues = ResidencyValidator::validate_config(&cfg);
assert!(issues
.iter()
.any(|i| i.field == "encryption_at_rest" && i.severity == IssueSeverity::Warning));
}
#[test]
fn test_transit_encryption_disabled_is_error() {
let mut cfg = us_standard_config();
cfg.encryption_in_transit = false;
let issues = ResidencyValidator::validate_config(&cfg);
assert!(issues
.iter()
.any(|i| i.field == "encryption_in_transit" && i.severity == IssueSeverity::Error));
}
#[test]
fn test_eu_gdpr_is_gdpr_compliant() {
let cfg = eu_gdpr_config();
assert!(ResidencyValidator::is_compliant(&cfg, "gdpr"));
}
#[test]
fn test_hipaa_config_is_hipaa_compliant() {
let cfg = hipaa_config();
assert!(ResidencyValidator::is_compliant(&cfg, "hipaa"));
}
#[test]
fn test_us_standard_is_not_hipaa_compliant() {
let cfg = us_standard_config();
assert!(!ResidencyValidator::is_compliant(&cfg, "hipaa"));
}
#[test]
fn test_eu_gdpr_is_iso27001_compliant() {
let cfg = eu_gdpr_config();
assert!(ResidencyValidator::is_compliant(&cfg, "iso27001"));
}
#[test]
fn test_eu_gdpr_is_dpga_compliant() {
let cfg = eu_gdpr_config();
assert!(ResidencyValidator::is_compliant(&cfg, "dpga"));
}
#[test]
fn test_hipaa_config_is_sox_compliant() {
let cfg = hipaa_config();
assert!(ResidencyValidator::is_compliant(&cfg, "sox"));
}
#[test]
fn test_us_standard_is_not_sox_compliant() {
let cfg = us_standard_config();
assert!(!ResidencyValidator::is_compliant(&cfg, "sox"));
}
#[test]
fn test_unknown_framework_is_not_compliant() {
let cfg = eu_gdpr_config();
assert!(!ResidencyValidator::is_compliant(&cfg, "unknown_framework"));
}
#[test]
fn test_compliance_case_insensitive() {
let cfg = eu_gdpr_config();
assert!(ResidencyValidator::is_compliant(&cfg, "GDPR"));
assert!(ResidencyValidator::is_compliant(&cfg, "Gdpr"));
}
#[test]
fn test_eu_gdpr_is_iso42001_compliant() {
let cfg = eu_gdpr_config();
assert!(ResidencyValidator::is_compliant(&cfg, "iso42001"));
}
#[test]
fn test_suggest_gdpr_config() {
let cfg = ResidencyValidator::suggest_config(DataRegion::EU, &["gdpr"]);
assert_eq!(cfg.region, DataRegion::EU);
assert_eq!(cfg.llm_routing_policy, LlmRoutingPolicy::SameRegion);
assert_eq!(cfg.pii_handling, PiiHandlingPolicy::Redact);
assert_eq!(cfg.data_retention_days, 30);
assert!(!cfg.cross_border_transfer);
assert!(ResidencyValidator::is_compliant(&cfg, "gdpr"));
}
#[test]
fn test_suggest_hipaa_config() {
let cfg = ResidencyValidator::suggest_config(DataRegion::US, &["hipaa"]);
assert_eq!(cfg.pii_handling, PiiHandlingPolicy::Encrypt);
assert_eq!(cfg.data_retention_days, 2555);
assert!(!cfg.cross_border_transfer);
assert!(ResidencyValidator::is_compliant(&cfg, "hipaa"));
}
#[test]
fn test_suggest_multi_framework() {
let cfg = ResidencyValidator::suggest_config(DataRegion::US, &["hipaa", "sox"]);
assert_eq!(cfg.data_retention_days, 2555);
assert!(ResidencyValidator::is_compliant(&cfg, "hipaa"));
assert!(ResidencyValidator::is_compliant(&cfg, "sox"));
}
#[test]
fn test_suggest_no_frameworks() {
let cfg = ResidencyValidator::suggest_config(DataRegion::APAC, &[]);
assert_eq!(cfg.region, DataRegion::APAC);
assert_eq!(cfg.data_retention_days, 90);
assert_eq!(cfg.llm_routing_policy, LlmRoutingPolicy::PreferRegion);
}
#[test]
fn test_suggest_custom_region() {
let cfg =
ResidencyValidator::suggest_config(DataRegion::Custom("me-south-1".to_string()), &[]);
assert_eq!(cfg.region, DataRegion::Custom("me-south-1".to_string()));
assert!(cfg.data_storage_location.contains("me-south-1"));
assert!(cfg.audit_data_location.contains("me-south-1"));
}
#[test]
fn test_config_roundtrip_json() {
let cfg = eu_gdpr_config();
let json = serde_json::to_string(&cfg).unwrap();
let deserialized: DataResidencyConfig = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized.region, cfg.region);
assert_eq!(deserialized.data_retention_days, cfg.data_retention_days);
assert_eq!(deserialized.pii_handling, cfg.pii_handling);
}
#[test]
fn test_data_region_display() {
assert_eq!(DataRegion::EU.to_string(), "EU");
assert_eq!(DataRegion::US.to_string(), "US");
assert_eq!(DataRegion::LATAM.to_string(), "LATAM");
assert_eq!(DataRegion::APAC.to_string(), "APAC");
assert_eq!(
DataRegion::Custom("me-south-1".to_string()).to_string(),
"Custom(me-south-1)"
);
}
#[test]
fn test_residency_issue_display() {
let issue = ResidencyIssue {
severity: IssueSeverity::Error,
field: "encryption_at_rest".into(),
message: "Must be enabled".into(),
};
assert_eq!(
issue.to_string(),
"[ERROR] encryption_at_rest: Must be enabled"
);
}
#[test]
fn test_default_config() {
let cfg = DataResidencyConfig::default();
assert_eq!(cfg.region, DataRegion::US);
assert!(cfg.encryption_at_rest);
assert!(cfg.encryption_in_transit);
assert_eq!(cfg.data_retention_days, 90);
assert_eq!(cfg.pii_handling, PiiHandlingPolicy::Redact);
assert!(!cfg.cross_border_transfer);
}
}