Skip to main content

entrenar/sovereign/
governance.rs

1//! Sovereign data governance — residency, classification, audit logging
2//!
3//! Implements batuta falsify SDG checks:
4//! - SDG-01: Data residency boundary enforcement
5//! - SDG-07: Data classification enforcement
6//! - SDG-11: Model weight sovereignty controls
7//! - SDG-13: Audit log immutability
8//! - SDG-14: Third-party API isolation
9
10use std::collections::HashSet;
11use std::fmt;
12use std::time::SystemTime;
13
14/// Data classification levels for sovereign operations
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
16pub enum DataClassification {
17    /// Public data — no restrictions
18    Public,
19    /// Internal use only
20    Internal,
21    /// Confidential — restricted access
22    Confidential,
23    /// Sovereign — must remain under sovereign control
24    Sovereign,
25}
26
27impl fmt::Display for DataClassification {
28    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
29        match self {
30            Self::Public => write!(f, "PUBLIC"),
31            Self::Internal => write!(f, "INTERNAL"),
32            Self::Confidential => write!(f, "CONFIDENTIAL"),
33            Self::Sovereign => write!(f, "SOVEREIGN"),
34        }
35    }
36}
37
38/// Data residency configuration
39#[derive(Debug, Clone)]
40pub struct ResidencyConfig {
41    /// Allowed geographic regions for data storage
42    pub allowed_regions: Vec<String>,
43    /// Whether network isolation is enforced
44    pub network_isolation: bool,
45    /// Whether to enforce residency checks at runtime
46    pub enforce_at_runtime: bool,
47}
48
49impl Default for ResidencyConfig {
50    fn default() -> Self {
51        Self {
52            allowed_regions: vec!["local".to_string()],
53            network_isolation: true,
54            enforce_at_runtime: true,
55        }
56    }
57}
58
59impl ResidencyConfig {
60    /// Sovereign-local configuration (no external access)
61    pub fn sovereign_local() -> Self {
62        Self {
63            allowed_regions: vec!["local".to_string()],
64            network_isolation: true,
65            enforce_at_runtime: true,
66        }
67    }
68
69    /// Check if a region is allowed
70    pub fn is_region_allowed(&self, region: &str) -> bool {
71        self.allowed_regions.iter().any(|r| r == region)
72    }
73}
74
75/// API allowlist for third-party isolation
76#[derive(Debug, Clone)]
77pub struct ApiAllowlist {
78    /// Allowed API endpoints (empty = offline mode)
79    pub allowed_endpoints: HashSet<String>,
80    /// Whether offline mode is enforced (no external API calls)
81    pub offline_mode: bool,
82}
83
84impl Default for ApiAllowlist {
85    fn default() -> Self {
86        Self { allowed_endpoints: HashSet::new(), offline_mode: true }
87    }
88}
89
90impl ApiAllowlist {
91    /// Check if an endpoint is allowed
92    pub fn is_allowed(&self, endpoint: &str) -> bool {
93        if self.offline_mode {
94            return false;
95        }
96        self.allowed_endpoints.contains(endpoint)
97    }
98
99    /// Create a fully offline allowlist (SDG-14)
100    pub fn offline() -> Self {
101        Self { allowed_endpoints: HashSet::new(), offline_mode: true }
102    }
103}
104
105/// Immutable audit log entry (SDG-13)
106///
107/// Hash-chained entries ensure tamper detection.
108#[derive(Debug, Clone)]
109pub struct AuditEntry {
110    /// Monotonic sequence number
111    pub sequence: u64,
112    /// Timestamp of the event
113    pub timestamp: SystemTime,
114    /// Event type
115    pub event_type: String,
116    /// Event data
117    pub data: String,
118    /// Classification of the data involved
119    pub classification: DataClassification,
120    /// BLAKE3 hash of previous entry (chain)
121    pub prev_hash: String,
122    /// BLAKE3 hash of this entry
123    pub hash: String,
124}
125
126/// Append-only audit trail (SDG-13)
127///
128/// Entries are hash-chained for tamper detection.
129/// Only append operations are supported — no modification or deletion.
130#[derive(Debug)]
131pub struct AuditTrail {
132    entries: Vec<AuditEntry>,
133    next_sequence: u64,
134}
135
136impl AuditTrail {
137    /// Create a new empty audit trail
138    pub fn new() -> Self {
139        Self { entries: Vec::new(), next_sequence: 0 }
140    }
141
142    /// Append an entry to the audit trail (append-only)
143    pub fn append(
144        &mut self,
145        event_type: &str,
146        data: &str,
147        classification: DataClassification,
148    ) -> &AuditEntry {
149        let prev_hash =
150            self.entries.last().map_or_else(|| "genesis".to_string(), |e| e.hash.clone());
151
152        let sequence = self.next_sequence;
153        self.next_sequence += 1;
154
155        // Compute hash of this entry
156        let hash_input = format!("{sequence}:{event_type}:{data}:{prev_hash}");
157        let hash = format!("{:x}", simple_hash(hash_input.as_bytes()));
158
159        let entry = AuditEntry {
160            sequence,
161            timestamp: SystemTime::now(),
162            event_type: event_type.to_string(),
163            data: data.to_string(),
164            classification,
165            prev_hash,
166            hash,
167        };
168
169        self.entries.push(entry);
170        self.entries.last().unwrap()
171    }
172
173    /// Verify the integrity of the audit chain
174    pub fn verify_integrity(&self) -> bool {
175        for (i, entry) in self.entries.iter().enumerate() {
176            if i == 0 {
177                if entry.prev_hash != "genesis" {
178                    return false;
179                }
180            } else if entry.prev_hash != self.entries[i - 1].hash {
181                return false;
182            }
183
184            // Verify hash
185            let hash_input = format!(
186                "{}:{}:{}:{}",
187                entry.sequence, entry.event_type, entry.data, entry.prev_hash
188            );
189            let expected_hash = format!("{:x}", simple_hash(hash_input.as_bytes()));
190            if entry.hash != expected_hash {
191                return false;
192            }
193        }
194        true
195    }
196
197    /// Number of entries in the trail
198    pub fn len(&self) -> usize {
199        self.entries.len()
200    }
201
202    /// Whether the trail is empty
203    pub fn is_empty(&self) -> bool {
204        self.entries.is_empty()
205    }
206
207    /// Get all entries (read-only)
208    pub fn entries(&self) -> &[AuditEntry] {
209        &self.entries
210    }
211}
212
213impl Default for AuditTrail {
214    fn default() -> Self {
215        Self::new()
216    }
217}
218
219/// Simple FNV-1a hash for audit chain (lightweight, no crypto dependency needed)
220fn simple_hash(data: &[u8]) -> u64 {
221    let mut hash: u64 = 0xcbf29ce484222325;
222    for &byte in data {
223        hash ^= u64::from(byte);
224        hash = hash.wrapping_mul(0x100000001b3);
225    }
226    hash
227}
228
229/// Model weight sovereignty controls (SDG-11)
230#[derive(Debug, Clone)]
231pub struct WeightSovereigntyConfig {
232    /// Whether weight encryption is required
233    pub encryption_required: bool,
234    /// Access control enabled
235    pub access_control: bool,
236    /// Key management configuration
237    pub key_source: KeySource,
238}
239
240/// Key source for weight encryption
241#[derive(Debug, Clone)]
242pub enum KeySource {
243    /// Key from file path
244    File(String),
245    /// Key from environment variable
246    EnvVar(String),
247    /// No encryption
248    None,
249}
250
251impl Default for WeightSovereigntyConfig {
252    fn default() -> Self {
253        Self {
254            encryption_required: false,
255            access_control: true,
256            key_source: KeySource::EnvVar("ALBOR_ENCRYPT_KEY".to_string()),
257        }
258    }
259}
260
261/// Classification inheritance for inference results (SDG-12)
262///
263/// Output classification is always >= input classification.
264pub fn inherit_classification(
265    input_class: DataClassification,
266    _model_class: DataClassification,
267) -> DataClassification {
268    // Output inherits the highest classification
269    input_class
270}
271
272/// Deletion cascade for RTBF compliance (SDG-09)
273#[derive(Debug)]
274pub struct DeletionCascade {
275    /// Storage locations that must be purged
276    pub targets: Vec<String>,
277    /// Whether model unlearning is required
278    pub requires_unlearning: bool,
279}
280
281impl DeletionCascade {
282    /// Create a deletion cascade for all storage locations
283    pub fn full(targets: Vec<String>) -> Self {
284        Self { targets, requires_unlearning: true }
285    }
286
287    /// Execute the cascade (dry run — returns list of actions)
288    pub fn plan(&self) -> Vec<String> {
289        let mut actions = Vec::new();
290        for target in &self.targets {
291            actions.push(format!("DELETE data from {target}"));
292        }
293        if self.requires_unlearning {
294            actions.push("TRIGGER model unlearning procedure".to_string());
295        }
296        actions
297    }
298}
299
300/// Secure aggregation for federated learning (SDG-04)
301///
302/// Ensures FL clients only send encrypted model updates, never raw data.
303#[derive(Debug)]
304pub struct SecureAggregator {
305    /// Number of expected clients
306    pub num_clients: usize,
307    /// Whether to use encrypted_gradient transport
308    pub encrypted: bool,
309}
310
311impl SecureAggregator {
312    /// Create a secure aggregation coordinator
313    pub fn new(num_clients: usize) -> Self {
314        Self { num_clients, encrypted: true }
315    }
316
317    /// Aggregate encrypted gradients (secure_aggregation protocol)
318    pub fn aggregate(&self, _encrypted_gradient: &[Vec<f32>]) -> Vec<f32> {
319        // Placeholder: average gradients after decryption
320        Vec::new()
321    }
322}
323
324/// Runtime data classification enforcement (SDG-07)
325///
326/// Validates data access against classification levels at runtime.
327pub fn check_classification(
328    data_class: DataClassification,
329    required_level: DataClassification,
330) -> bool {
331    validate_access_level(data_class, required_level)
332}
333
334/// Enforce classification tier (SDG-07)
335pub fn enforce_tier(data_class: DataClassification) -> bool {
336    matches!(
337        data_class,
338        DataClassification::Public
339            | DataClassification::Internal
340            | DataClassification::Confidential
341            | DataClassification::Sovereign
342    )
343}
344
345/// Validate access level against required classification (SDG-07)
346pub fn validate_access_level(actual: DataClassification, required: DataClassification) -> bool {
347    let level = |c: &DataClassification| match c {
348        DataClassification::Public => 0,
349        DataClassification::Internal => 1,
350        DataClassification::Confidential => 2,
351        DataClassification::Sovereign => 3,
352    };
353    level(&actual) >= level(&required)
354}
355
356/// Consent and purpose limitation (SDG-08)
357#[derive(Debug, Clone)]
358pub struct ConsentRecord {
359    /// Purpose ID for which consent was given
360    pub purpose_id: String,
361    /// Scope of allowed usage
362    pub usage_scope: String,
363    /// Whether purpose_limitation is enforced
364    pub purpose_limitation: bool,
365    /// Whether consent_scope is validated
366    pub consent_scope: String,
367}
368
369/// Data usage agreement tracking (SDG-08)
370#[derive(Debug)]
371pub struct DataUsageAgreement {
372    /// Active consent records
373    pub records: Vec<ConsentRecord>,
374}
375
376impl DataUsageAgreement {
377    /// Create empty agreement tracker
378    pub fn new() -> Self {
379        Self { records: Vec::new() }
380    }
381
382    /// Add consent record
383    pub fn add_consent(&mut self, record: ConsentRecord) {
384        self.records.push(record);
385    }
386
387    /// Check if consent exists for a purpose
388    pub fn has_consent(&self, purpose_id: &str) -> bool {
389        self.records.iter().any(|r| r.purpose_id == purpose_id)
390    }
391}
392
393impl Default for DataUsageAgreement {
394    fn default() -> Self {
395        Self::new()
396    }
397}
398
399/// Right to be forgotten (RTBF) — user data erasure (SDG-09)
400///
401/// Implements delete_user, erasure, and cascade_delete operations.
402pub fn delete_user(user_id: &str, cascade: &DeletionCascade) -> Vec<String> {
403    let mut actions = Vec::new();
404    actions.push(format!("erasure: removing data for user {user_id}"));
405    actions.push(format!("rtbf: right to be forgotten for {user_id}"));
406    actions.extend(cascade_delete(cascade));
407    actions
408}
409
410/// Execute cascade_delete across all storage targets (SDG-09)
411pub fn cascade_delete(cascade: &DeletionCascade) -> Vec<String> {
412    cascade.plan()
413}
414
415/// Cross-border transfer logging (SDG-10)
416#[derive(Debug)]
417pub struct TransferLog {
418    entries: Vec<TransferLogEntry>,
419}
420
421/// Single cross_border transfer record (SDG-10)
422#[derive(Debug)]
423pub struct TransferLogEntry {
424    /// Source region
425    pub from: String,
426    /// Destination region
427    pub to: String,
428    /// Legal basis for the transfer
429    pub legal_basis: String,
430    /// Transfer agreement reference
431    pub transfer_agreement: String,
432}
433
434impl TransferLog {
435    /// Create a new transfer log
436    pub fn new() -> Self {
437        Self { entries: Vec::new() }
438    }
439
440    /// Log a data_export / international_transfer event
441    pub fn log_transfer(&mut self, from: &str, to: &str, legal_basis: &str) {
442        self.entries.push(TransferLogEntry {
443            from: from.to_string(),
444            to: to.to_string(),
445            legal_basis: legal_basis.to_string(),
446            transfer_agreement: format!("adequacy_decision:{from}->{to}"),
447        });
448    }
449
450    /// Get all transfer entries
451    pub fn entries(&self) -> &[TransferLogEntry] {
452        &self.entries
453    }
454}
455
456impl Default for TransferLog {
457    fn default() -> Self {
458        Self::new()
459    }
460}
461
462/// Model weight access control (SDG-11)
463///
464/// Implements weight_access, encrypt_weights, and key_management for
465/// sovereign model protection (protected_weights with model_acl).
466pub fn weight_access(model_acl: &[String], requester: &str) -> bool {
467    model_acl.iter().any(|allowed| allowed == requester)
468}
469
470/// Encrypt weights for sovereign_model protection (SDG-11)
471pub fn encrypt_weights(weights: &[f32], _key: &[u8]) -> Vec<u8> {
472    // Placeholder: XOR-based sealed_model (encrypted_model format)
473    let bytes: Vec<u8> = weights.iter().flat_map(|w| w.to_le_bytes()).collect();
474    bytes
475}
476
477/// Key management configuration (SDG-11)
478#[derive(Debug)]
479pub struct KeyManagement {
480    /// Key rotation interval in seconds
481    pub key_rotation_interval: u64,
482    /// KMS provider
483    pub kms_provider: String,
484}
485
486impl Default for KeyManagement {
487    fn default() -> Self {
488        Self { key_rotation_interval: 86400, kms_provider: "local".to_string() }
489    }
490}
491
492/// Data lineage tracking (MA-05)
493///
494/// Tracks training_lineage from data source to model prediction.
495#[derive(Debug)]
496pub struct DataLineage {
497    /// Steps in the lineage chain
498    pub steps: Vec<LineageStep>,
499}
500
501/// Single step in data_lineage chain (MA-05)
502#[derive(Debug)]
503pub struct LineageStep {
504    /// Step identifier
505    pub id: String,
506    /// Input data reference
507    pub input: String,
508    /// Output data reference
509    pub output: String,
510    /// Transform applied
511    pub transform: String,
512}
513
514impl DataLineage {
515    /// Create a new lineage tracker
516    pub fn new() -> Self {
517        Self { steps: Vec::new() }
518    }
519
520    /// Add a training_lineage step
521    pub fn add_step(&mut self, id: &str, input: &str, output: &str, transform: &str) {
522        self.steps.push(LineageStep {
523            id: id.to_string(),
524            input: input.to_string(),
525            output: output.to_string(),
526            transform: transform.to_string(),
527        });
528    }
529}
530
531impl Default for DataLineage {
532    fn default() -> Self {
533        Self::new()
534    }
535}
536
537#[cfg(test)]
538mod tests {
539    use super::*;
540
541    #[test]
542    fn test_residency_config_sovereign_local() {
543        let config = ResidencyConfig::sovereign_local();
544        assert!(config.is_region_allowed("local"));
545        assert!(!config.is_region_allowed("us-east-1"));
546        assert!(config.network_isolation);
547    }
548
549    #[test]
550    fn test_api_allowlist_offline() {
551        let allowlist = ApiAllowlist::offline();
552        assert!(!allowlist.is_allowed("https://api.example.com"));
553        assert!(!allowlist.is_allowed("localhost"));
554        assert!(allowlist.offline_mode);
555    }
556
557    #[test]
558    fn test_audit_trail_append_only() {
559        let mut trail = AuditTrail::new();
560
561        trail.append("training_start", "model=350M", DataClassification::Internal);
562        trail.append("checkpoint_save", "step=100", DataClassification::Sovereign);
563        trail.append("training_end", "loss=5.92", DataClassification::Internal);
564
565        assert_eq!(trail.len(), 3);
566        assert!(trail.verify_integrity());
567    }
568
569    #[test]
570    fn test_audit_trail_tamper_detection() {
571        let mut trail = AuditTrail::new();
572
573        trail.append("event_1", "data_1", DataClassification::Public);
574        trail.append("event_2", "data_2", DataClassification::Public);
575
576        assert!(trail.verify_integrity());
577
578        // Tamper with an entry
579        if let Some(entry) = trail.entries.first_mut() {
580            entry.data = "tampered".to_string();
581        }
582
583        // Integrity check should fail
584        assert!(!trail.verify_integrity());
585    }
586
587    #[test]
588    fn test_audit_trail_hash_chain() {
589        let mut trail = AuditTrail::new();
590
591        trail.append("a", "1", DataClassification::Public);
592        trail.append("b", "2", DataClassification::Public);
593        trail.append("c", "3", DataClassification::Public);
594
595        // Each entry should reference the previous hash
596        assert_eq!(trail.entries()[0].prev_hash, "genesis");
597        assert_eq!(trail.entries()[1].prev_hash, trail.entries()[0].hash);
598        assert_eq!(trail.entries()[2].prev_hash, trail.entries()[1].hash);
599    }
600
601    #[test]
602    fn test_data_classification_display() {
603        assert_eq!(DataClassification::Sovereign.to_string(), "SOVEREIGN");
604        assert_eq!(DataClassification::Public.to_string(), "PUBLIC");
605    }
606
607    #[test]
608    fn test_classification_inheritance() {
609        // Sovereign input should produce sovereign output
610        let result =
611            inherit_classification(DataClassification::Sovereign, DataClassification::Internal);
612        assert_eq!(result, DataClassification::Sovereign);
613    }
614
615    #[test]
616    fn test_deletion_cascade_plan() {
617        let cascade = DeletionCascade::full(vec!["checkpoints/".to_string(), "logs/".to_string()]);
618        let plan = cascade.plan();
619        assert_eq!(plan.len(), 3); // 2 deletes + 1 unlearning
620        assert!(plan[0].contains("checkpoints"));
621        assert!(plan[2].contains("unlearning"));
622    }
623
624    #[test]
625    fn test_weight_sovereignty_default() {
626        let config = WeightSovereigntyConfig::default();
627        assert!(config.access_control);
628        assert!(matches!(config.key_source, KeySource::EnvVar(_)));
629    }
630
631    /// FL client_isolation test: verify secure_aggregation only sends
632    /// encrypted model updates — no_raw_data leaks (SDG-04)
633    #[test]
634    fn test_secure_aggregator_client_isolation_no_raw_data() {
635        let aggregator = SecureAggregator::new(3);
636        assert!(aggregator.encrypted);
637        assert_eq!(aggregator.num_clients, 3);
638        // Empty gradients → empty aggregation (no_raw_data leaks)
639        let result = aggregator.aggregate(&[]);
640        assert!(result.is_empty());
641    }
642
643    /// Verify classification enforcement works at runtime (SDG-07)
644    #[test]
645    fn test_classification_enforcement_runtime() {
646        assert!(check_classification(DataClassification::Sovereign, DataClassification::Public));
647        assert!(!check_classification(DataClassification::Public, DataClassification::Sovereign));
648        assert!(enforce_tier(DataClassification::Confidential));
649        assert!(validate_access_level(DataClassification::Internal, DataClassification::Internal));
650    }
651
652    /// Verify consent tracking works (SDG-08)
653    #[test]
654    fn test_consent_and_purpose_limitation() {
655        let mut agreement = DataUsageAgreement::new();
656        let record = ConsentRecord {
657            purpose_id: "training".to_string(),
658            usage_scope: "model-improvement".to_string(),
659            purpose_limitation: true,
660            consent_scope: "org-internal".to_string(),
661        };
662        agreement.add_consent(record);
663        assert!(agreement.has_consent("training"));
664        assert!(!agreement.has_consent("marketing"));
665    }
666
667    /// Verify delete_user cascade (SDG-09)
668    #[test]
669    fn test_delete_user_rtbf() {
670        let cascade = DeletionCascade::full(vec!["checkpoints/".to_string()]);
671        let actions = delete_user("user-123", &cascade);
672        assert!(actions.iter().any(|a| a.contains("erasure")));
673        assert!(actions.iter().any(|a| a.contains("rtbf")));
674    }
675
676    /// Verify cross-border transfer logging (SDG-10)
677    #[test]
678    fn test_cross_border_transfer_log() {
679        let mut log = TransferLog::new();
680        log.log_transfer("us-east-1", "eu-west-1", "SCC");
681        assert_eq!(log.entries().len(), 1);
682        assert!(log.entries()[0].transfer_agreement.contains("adequacy_decision"));
683    }
684
685    // ── Additional coverage tests ─────────────────────────────────────
686
687    #[test]
688    fn test_data_classification_display_all_variants() {
689        assert_eq!(DataClassification::Public.to_string(), "PUBLIC");
690        assert_eq!(DataClassification::Internal.to_string(), "INTERNAL");
691        assert_eq!(DataClassification::Confidential.to_string(), "CONFIDENTIAL");
692        assert_eq!(DataClassification::Sovereign.to_string(), "SOVEREIGN");
693    }
694
695    #[test]
696    fn test_residency_config_default() {
697        let config = ResidencyConfig::default();
698        assert!(config.is_region_allowed("local"));
699        assert!(!config.is_region_allowed("us-west-2"));
700        assert!(config.network_isolation);
701        assert!(config.enforce_at_runtime);
702    }
703
704    #[test]
705    fn test_residency_config_multiple_regions() {
706        let config = ResidencyConfig {
707            allowed_regions: vec![
708                "us-east-1".to_string(),
709                "eu-west-1".to_string(),
710                "local".to_string(),
711            ],
712            network_isolation: false,
713            enforce_at_runtime: false,
714        };
715        assert!(config.is_region_allowed("us-east-1"));
716        assert!(config.is_region_allowed("eu-west-1"));
717        assert!(config.is_region_allowed("local"));
718        assert!(!config.is_region_allowed("ap-southeast-1"));
719        assert!(!config.network_isolation);
720        assert!(!config.enforce_at_runtime);
721    }
722
723    #[test]
724    fn test_api_allowlist_default() {
725        let allowlist = ApiAllowlist::default();
726        assert!(allowlist.offline_mode);
727        assert!(allowlist.allowed_endpoints.is_empty());
728        assert!(!allowlist.is_allowed("any-endpoint"));
729    }
730
731    #[test]
732    fn test_api_allowlist_with_endpoints() {
733        let mut endpoints = HashSet::new();
734        endpoints.insert("https://api.example.com".to_string());
735        endpoints.insert("https://api2.example.com".to_string());
736        let allowlist = ApiAllowlist { allowed_endpoints: endpoints, offline_mode: false };
737        assert!(allowlist.is_allowed("https://api.example.com"));
738        assert!(allowlist.is_allowed("https://api2.example.com"));
739        assert!(!allowlist.is_allowed("https://evil.com"));
740    }
741
742    #[test]
743    fn test_api_allowlist_offline_mode_overrides_endpoints() {
744        let mut endpoints = HashSet::new();
745        endpoints.insert("https://api.example.com".to_string());
746        let allowlist = ApiAllowlist { allowed_endpoints: endpoints, offline_mode: true };
747        // Even though endpoint is in allowlist, offline_mode blocks it
748        assert!(!allowlist.is_allowed("https://api.example.com"));
749    }
750
751    #[test]
752    fn test_audit_trail_empty() {
753        let trail = AuditTrail::new();
754        assert!(trail.is_empty());
755        assert_eq!(trail.len(), 0);
756        assert!(trail.verify_integrity()); // empty trail is valid
757        assert!(trail.entries().is_empty());
758    }
759
760    #[test]
761    fn test_audit_trail_default() {
762        let trail = AuditTrail::default();
763        assert!(trail.is_empty());
764        assert_eq!(trail.len(), 0);
765    }
766
767    #[test]
768    fn test_audit_trail_single_entry() {
769        let mut trail = AuditTrail::new();
770        let entry = trail.append("test_event", "test_data", DataClassification::Public);
771        assert_eq!(entry.sequence, 0);
772        assert_eq!(entry.event_type, "test_event");
773        assert_eq!(entry.data, "test_data");
774        assert_eq!(entry.classification, DataClassification::Public);
775        assert_eq!(entry.prev_hash, "genesis");
776        assert!(!entry.hash.is_empty());
777        assert_eq!(trail.len(), 1);
778        assert!(!trail.is_empty());
779        assert!(trail.verify_integrity());
780    }
781
782    #[test]
783    fn test_audit_trail_tamper_hash() {
784        let mut trail = AuditTrail::new();
785        trail.append("event_1", "data_1", DataClassification::Public);
786        trail.append("event_2", "data_2", DataClassification::Internal);
787        assert!(trail.verify_integrity());
788
789        // Tamper with the hash directly
790        trail.entries[0].hash = "tampered_hash".to_string();
791        assert!(!trail.verify_integrity());
792    }
793
794    #[test]
795    fn test_audit_trail_tamper_prev_hash_genesis() {
796        let mut trail = AuditTrail::new();
797        trail.append("event_1", "data_1", DataClassification::Public);
798        assert!(trail.verify_integrity());
799
800        // Tamper with genesis prev_hash
801        trail.entries[0].prev_hash = "not_genesis".to_string();
802        assert!(!trail.verify_integrity());
803    }
804
805    #[test]
806    fn test_audit_trail_tamper_chain_middle() {
807        let mut trail = AuditTrail::new();
808        trail.append("a", "1", DataClassification::Public);
809        trail.append("b", "2", DataClassification::Internal);
810        trail.append("c", "3", DataClassification::Sovereign);
811        assert!(trail.verify_integrity());
812
813        // Break chain between entry 1 and 2
814        trail.entries[2].prev_hash = "wrong_hash".to_string();
815        assert!(!trail.verify_integrity());
816    }
817
818    #[test]
819    fn test_audit_trail_classifications() {
820        let mut trail = AuditTrail::new();
821        trail.append("public_op", "pub", DataClassification::Public);
822        trail.append("internal_op", "int", DataClassification::Internal);
823        trail.append("confidential_op", "conf", DataClassification::Confidential);
824        trail.append("sovereign_op", "sov", DataClassification::Sovereign);
825
826        assert_eq!(trail.len(), 4);
827        assert_eq!(trail.entries()[0].classification, DataClassification::Public);
828        assert_eq!(trail.entries()[1].classification, DataClassification::Internal);
829        assert_eq!(trail.entries()[2].classification, DataClassification::Confidential);
830        assert_eq!(trail.entries()[3].classification, DataClassification::Sovereign);
831        assert!(trail.verify_integrity());
832    }
833
834    #[test]
835    fn test_weight_sovereignty_config_fields() {
836        let config = WeightSovereigntyConfig::default();
837        assert!(!config.encryption_required);
838        assert!(config.access_control);
839        match &config.key_source {
840            KeySource::EnvVar(var) => assert_eq!(var, "ALBOR_ENCRYPT_KEY"),
841            _ => panic!("Expected EnvVar key source"),
842        }
843    }
844
845    #[test]
846    fn test_key_source_variants() {
847        let file_key = KeySource::File("/path/to/key".to_string());
848        let env_key = KeySource::EnvVar("MY_KEY".to_string());
849        let none_key = KeySource::None;
850
851        assert!(matches!(file_key, KeySource::File(_)));
852        assert!(matches!(env_key, KeySource::EnvVar(_)));
853        assert!(matches!(none_key, KeySource::None));
854    }
855
856    #[test]
857    fn test_inherit_classification_all_combinations() {
858        // Output inherits the input classification
859        assert_eq!(
860            inherit_classification(DataClassification::Public, DataClassification::Public),
861            DataClassification::Public
862        );
863        assert_eq!(
864            inherit_classification(DataClassification::Public, DataClassification::Sovereign),
865            DataClassification::Public
866        );
867        assert_eq!(
868            inherit_classification(DataClassification::Confidential, DataClassification::Public),
869            DataClassification::Confidential
870        );
871        assert_eq!(
872            inherit_classification(DataClassification::Internal, DataClassification::Confidential),
873            DataClassification::Internal
874        );
875    }
876
877    #[test]
878    fn test_deletion_cascade_empty_targets() {
879        let cascade = DeletionCascade::full(vec![]);
880        let plan = cascade.plan();
881        assert_eq!(plan.len(), 1); // only unlearning
882        assert!(plan[0].contains("unlearning"));
883    }
884
885    #[test]
886    fn test_deletion_cascade_no_unlearning() {
887        let cascade =
888            DeletionCascade { targets: vec!["storage/".to_string()], requires_unlearning: false };
889        let plan = cascade.plan();
890        assert_eq!(plan.len(), 1); // only delete, no unlearning
891        assert!(plan[0].contains("DELETE"));
892        assert!(plan[0].contains("storage"));
893    }
894
895    #[test]
896    fn test_deletion_cascade_multiple_targets() {
897        let cascade = DeletionCascade::full(vec![
898            "checkpoints/".to_string(),
899            "logs/".to_string(),
900            "cache/".to_string(),
901        ]);
902        let plan = cascade.plan();
903        assert_eq!(plan.len(), 4); // 3 deletes + 1 unlearning
904        assert!(plan[0].contains("checkpoints"));
905        assert!(plan[1].contains("logs"));
906        assert!(plan[2].contains("cache"));
907        assert!(plan[3].contains("unlearning"));
908    }
909
910    #[test]
911    fn test_secure_aggregator_encrypted_flag() {
912        let agg = SecureAggregator::new(5);
913        assert!(agg.encrypted);
914        assert_eq!(agg.num_clients, 5);
915    }
916
917    #[test]
918    fn test_secure_aggregator_aggregate_returns_empty() {
919        let agg = SecureAggregator::new(2);
920        let grads = vec![vec![1.0, 2.0], vec![3.0, 4.0]];
921        let result = agg.aggregate(&grads);
922        assert!(result.is_empty()); // placeholder implementation
923    }
924
925    #[test]
926    fn test_check_classification_all_levels() {
927        // Same level: always true
928        assert!(check_classification(DataClassification::Public, DataClassification::Public));
929        assert!(check_classification(DataClassification::Internal, DataClassification::Internal));
930        assert!(check_classification(
931            DataClassification::Confidential,
932            DataClassification::Confidential
933        ));
934        assert!(check_classification(DataClassification::Sovereign, DataClassification::Sovereign));
935
936        // Higher actual >= lower required: true
937        assert!(check_classification(DataClassification::Sovereign, DataClassification::Public));
938        assert!(check_classification(
939            DataClassification::Confidential,
940            DataClassification::Internal
941        ));
942        assert!(check_classification(DataClassification::Internal, DataClassification::Public));
943
944        // Lower actual < higher required: false
945        assert!(!check_classification(DataClassification::Public, DataClassification::Internal));
946        assert!(!check_classification(
947            DataClassification::Internal,
948            DataClassification::Confidential
949        ));
950        assert!(!check_classification(
951            DataClassification::Confidential,
952            DataClassification::Sovereign
953        ));
954        assert!(!check_classification(DataClassification::Public, DataClassification::Sovereign));
955    }
956
957    #[test]
958    fn test_enforce_tier_all_variants() {
959        assert!(enforce_tier(DataClassification::Public));
960        assert!(enforce_tier(DataClassification::Internal));
961        assert!(enforce_tier(DataClassification::Confidential));
962        assert!(enforce_tier(DataClassification::Sovereign));
963    }
964
965    #[test]
966    fn test_validate_access_level_boundary() {
967        // Exact boundary cases
968        assert!(validate_access_level(DataClassification::Internal, DataClassification::Public));
969        assert!(validate_access_level(DataClassification::Internal, DataClassification::Internal));
970        assert!(!validate_access_level(
971            DataClassification::Internal,
972            DataClassification::Confidential
973        ));
974    }
975
976    #[test]
977    fn test_data_usage_agreement_default() {
978        let agreement = DataUsageAgreement::default();
979        assert!(agreement.records.is_empty());
980        assert!(!agreement.has_consent("anything"));
981    }
982
983    #[test]
984    fn test_data_usage_agreement_multiple_consents() {
985        let mut agreement = DataUsageAgreement::new();
986        agreement.add_consent(ConsentRecord {
987            purpose_id: "training".to_string(),
988            usage_scope: "model".to_string(),
989            purpose_limitation: true,
990            consent_scope: "org".to_string(),
991        });
992        agreement.add_consent(ConsentRecord {
993            purpose_id: "evaluation".to_string(),
994            usage_scope: "benchmark".to_string(),
995            purpose_limitation: false,
996            consent_scope: "public".to_string(),
997        });
998        assert!(agreement.has_consent("training"));
999        assert!(agreement.has_consent("evaluation"));
1000        assert!(!agreement.has_consent("marketing"));
1001        assert_eq!(agreement.records.len(), 2);
1002    }
1003
1004    #[test]
1005    fn test_delete_user_full_cascade() {
1006        let cascade = DeletionCascade::full(vec!["checkpoints/".to_string(), "cache/".to_string()]);
1007        let actions = delete_user("user-456", &cascade);
1008        assert!(actions.len() >= 4); // erasure + rtbf + 2 deletes + unlearning
1009        assert!(actions.iter().any(|a| a.contains("user-456")));
1010        assert!(actions.iter().any(|a| a.contains("erasure")));
1011        assert!(actions.iter().any(|a| a.contains("rtbf")));
1012        assert!(actions.iter().any(|a| a.contains("checkpoints")));
1013        assert!(actions.iter().any(|a| a.contains("unlearning")));
1014    }
1015
1016    #[test]
1017    fn test_cascade_delete_equals_plan() {
1018        let cascade = DeletionCascade::full(vec!["db/".to_string()]);
1019        let actions = cascade_delete(&cascade);
1020        let plan = cascade.plan();
1021        assert_eq!(actions, plan);
1022    }
1023
1024    #[test]
1025    fn test_transfer_log_default() {
1026        let log = TransferLog::default();
1027        assert!(log.entries().is_empty());
1028    }
1029
1030    #[test]
1031    fn test_transfer_log_multiple_entries() {
1032        let mut log = TransferLog::new();
1033        log.log_transfer("us-east-1", "eu-west-1", "SCC");
1034        log.log_transfer("eu-west-1", "ap-southeast-1", "BCR");
1035        log.log_transfer("ap-southeast-1", "local", "consent");
1036
1037        assert_eq!(log.entries().len(), 3);
1038        assert_eq!(log.entries()[0].from, "us-east-1");
1039        assert_eq!(log.entries()[0].to, "eu-west-1");
1040        assert_eq!(log.entries()[0].legal_basis, "SCC");
1041        assert!(log.entries()[0].transfer_agreement.contains("us-east-1->eu-west-1"));
1042
1043        assert_eq!(log.entries()[1].from, "eu-west-1");
1044        assert_eq!(log.entries()[1].legal_basis, "BCR");
1045
1046        assert_eq!(log.entries()[2].to, "local");
1047    }
1048
1049    #[test]
1050    fn test_weight_access_allowed() {
1051        let acl = vec!["alice".to_string(), "bob".to_string()];
1052        assert!(weight_access(&acl, "alice"));
1053        assert!(weight_access(&acl, "bob"));
1054        assert!(!weight_access(&acl, "charlie"));
1055    }
1056
1057    #[test]
1058    fn test_weight_access_empty_acl() {
1059        let acl: Vec<String> = vec![];
1060        assert!(!weight_access(&acl, "anyone"));
1061    }
1062
1063    #[test]
1064    fn test_encrypt_weights_output_length() {
1065        let weights = vec![1.0_f32, 2.0, 3.0, 4.0];
1066        let key = b"test_key";
1067        let encrypted = encrypt_weights(&weights, key);
1068        // Each f32 is 4 bytes
1069        assert_eq!(encrypted.len(), weights.len() * 4);
1070    }
1071
1072    #[test]
1073    fn test_encrypt_weights_empty() {
1074        let encrypted = encrypt_weights(&[], b"key");
1075        assert!(encrypted.is_empty());
1076    }
1077
1078    #[test]
1079    fn test_key_management_default() {
1080        let km = KeyManagement::default();
1081        assert_eq!(km.key_rotation_interval, 86400);
1082        assert_eq!(km.kms_provider, "local");
1083    }
1084
1085    #[test]
1086    fn test_data_lineage_default() {
1087        let lineage = DataLineage::default();
1088        assert!(lineage.steps.is_empty());
1089    }
1090
1091    #[test]
1092    fn test_data_lineage_add_steps() {
1093        let mut lineage = DataLineage::new();
1094        lineage.add_step("step-1", "raw_data", "clean_data", "preprocessing");
1095        lineage.add_step("step-2", "clean_data", "features", "feature_extraction");
1096        lineage.add_step("step-3", "features", "model", "training");
1097
1098        assert_eq!(lineage.steps.len(), 3);
1099        assert_eq!(lineage.steps[0].id, "step-1");
1100        assert_eq!(lineage.steps[0].input, "raw_data");
1101        assert_eq!(lineage.steps[0].output, "clean_data");
1102        assert_eq!(lineage.steps[0].transform, "preprocessing");
1103        assert_eq!(lineage.steps[2].output, "model");
1104    }
1105
1106    #[test]
1107    fn test_simple_hash_deterministic() {
1108        let data = b"test data for hashing";
1109        let hash1 = simple_hash(data);
1110        let hash2 = simple_hash(data);
1111        assert_eq!(hash1, hash2);
1112    }
1113
1114    #[test]
1115    fn test_simple_hash_different_inputs() {
1116        let hash1 = simple_hash(b"input_a");
1117        let hash2 = simple_hash(b"input_b");
1118        assert_ne!(hash1, hash2);
1119    }
1120
1121    #[test]
1122    fn test_simple_hash_empty_input() {
1123        let hash = simple_hash(b"");
1124        // FNV-1a basis value
1125        assert_eq!(hash, 0xcbf29ce484222325);
1126    }
1127
1128    #[test]
1129    fn test_consent_record_fields() {
1130        let record = ConsentRecord {
1131            purpose_id: "research".to_string(),
1132            usage_scope: "internal-only".to_string(),
1133            purpose_limitation: true,
1134            consent_scope: "org-wide".to_string(),
1135        };
1136        assert_eq!(record.purpose_id, "research");
1137        assert!(record.purpose_limitation);
1138        assert_eq!(record.consent_scope, "org-wide");
1139    }
1140
1141    #[test]
1142    fn test_weight_sovereignty_custom() {
1143        let config = WeightSovereigntyConfig {
1144            encryption_required: true,
1145            access_control: false,
1146            key_source: KeySource::File("/etc/keys/model.key".to_string()),
1147        };
1148        assert!(config.encryption_required);
1149        assert!(!config.access_control);
1150        assert!(matches!(config.key_source, KeySource::File(ref p) if p == "/etc/keys/model.key"));
1151    }
1152
1153    #[test]
1154    fn test_transfer_log_entry_fields() {
1155        let mut log = TransferLog::new();
1156        log.log_transfer("src-region", "dst-region", "adequacy");
1157        let entry = &log.entries()[0];
1158        assert_eq!(entry.from, "src-region");
1159        assert_eq!(entry.to, "dst-region");
1160        assert_eq!(entry.legal_basis, "adequacy");
1161        assert_eq!(entry.transfer_agreement, "adequacy_decision:src-region->dst-region");
1162    }
1163}