Skip to main content

batuta/falsification/
sovereign_data.rs

1//! Section 1: Sovereign Data Governance (SDG-01 to SDG-15)
2//!
3//! Implements the Five Pillars verification: Data Residency, Privacy, Legal Controls.
4//!
5//! # TPS Principles
6//!
7//! - **Jidoka**: Automatic boundary violation detection
8//! - **Poka-Yoke**: Classification-based routing, privacy by design
9//! - **Muda**: Data inventory hygiene, prevent hoarding liability
10//! - **Genchi Genbutsu**: Verify physical data locations
11
12use super::types::{CheckItem, Evidence, EvidenceType, Severity};
13use std::path::Path;
14use std::time::Instant;
15
16/// Evaluate all Sovereign Data Governance checks.
17pub fn evaluate_all(project_path: &Path) -> Vec<CheckItem> {
18    vec![
19        check_data_residency_boundary(project_path),
20        check_data_inventory_completeness(project_path),
21        check_privacy_preserving_computation(project_path),
22        check_federated_learning_isolation(project_path),
23        check_supply_chain_provenance(project_path),
24        check_vpc_isolation(project_path),
25        check_data_classification_enforcement(project_path),
26        check_consent_purpose_limitation(project_path),
27        check_rtbf_compliance(project_path),
28        check_cross_border_logging(project_path),
29        check_model_weight_sovereignty(project_path),
30        check_inference_classification(project_path),
31        check_audit_log_immutability(project_path),
32        check_third_party_isolation(project_path),
33        check_secure_computation(project_path),
34    ]
35}
36
37/// SDG-01: Data Residency Boundary Enforcement
38///
39/// **Claim:** Sovereign-critical data never crosses defined geographic boundaries.
40///
41/// **Rejection Criteria (Critical):**
42/// - Any network call to non-compliant regions during Sovereign-tier operations
43pub fn check_data_residency_boundary(project_path: &Path) -> CheckItem {
44    let start = Instant::now();
45    let mut item = CheckItem::new(
46        "SDG-01",
47        "Data Residency Boundary Enforcement",
48        "Sovereign-critical data never crosses geographic boundaries",
49    )
50    .with_severity(Severity::Critical)
51    .with_tps("Jidoka — automatic boundary violation detection");
52
53    // Check for residency configuration
54    let config_files = [
55        project_path.join("config/residency.yaml"),
56        project_path.join("config/residency.toml"),
57        project_path.join(".sovereignty/residency.yaml"),
58    ];
59
60    let has_residency_config = config_files.iter().any(|p| p.exists());
61
62    // Check for VPC/network isolation configuration
63    let infra_files = [
64        project_path.join("infrastructure/vpc.tf"),
65        project_path.join("terraform/vpc.tf"),
66        project_path.join("infra/network.yaml"),
67    ];
68
69    let has_network_isolation = infra_files.iter().any(|p| p.exists());
70
71    // Check for residency-aware code patterns
72    let has_residency_checks = check_for_pattern(
73        project_path,
74        &["residency", "region_check", "boundary_enforce", "geo_fence"],
75    );
76
77    item = item.with_evidence(Evidence {
78        evidence_type: EvidenceType::StaticAnalysis,
79        description: format!(
80            "Residency: config={}, network_isolation={}, code_checks={}",
81            has_residency_config, has_network_isolation, has_residency_checks
82        ),
83        data: None,
84        files: Vec::new(),
85    });
86
87    let is_cli_only = !has_network_code(project_path);
88    item = apply_check_outcome(
89        item,
90        &[
91            (has_residency_config && has_network_isolation, None),
92            (
93                has_residency_config || has_residency_checks,
94                Some("Partial residency enforcement (missing network isolation)"),
95            ),
96            (is_cli_only, None),
97            (true, Some("No explicit residency configuration")),
98        ],
99    );
100
101    item.finish_timed(start)
102}
103
104/// SDG-02: Data Inventory Completeness
105///
106/// **Claim:** All ingested data has documented purpose, classification, and lifecycle policy.
107///
108/// **Rejection Criteria (Major):**
109/// - Any data asset without classification tag or lifecycle policy
110pub fn check_data_inventory_completeness(project_path: &Path) -> CheckItem {
111    let start = Instant::now();
112    let mut item = CheckItem::new(
113        "SDG-02",
114        "Data Inventory Completeness",
115        "All data has documented purpose, classification, and lifecycle",
116    )
117    .with_severity(Severity::Major)
118    .with_tps("Muda (Inventory waste) — prevent data hoarding");
119
120    // Check for data catalog/inventory
121    let inventory_files = [
122        project_path.join("data/inventory.yaml"),
123        project_path.join("docs/data-catalog.md"),
124        project_path.join(".datasheets/"),
125        project_path.join("data/schema/"),
126    ];
127
128    let has_inventory = inventory_files.iter().any(|p| p.exists());
129
130    // Check for data classification schema
131    let has_classification = check_for_pattern(
132        project_path,
133        &["DataClassification", "data_class", "classification_level"],
134    );
135
136    // Check for lifecycle policies
137    let has_lifecycle = check_for_pattern(
138        project_path,
139        &["lifecycle_policy", "retention_days", "data_ttl", "expiration"],
140    );
141
142    item = item.with_evidence(Evidence {
143        evidence_type: EvidenceType::StaticAnalysis,
144        description: format!(
145            "Data inventory: catalog={}, classification={}, lifecycle={}",
146            has_inventory, has_classification, has_lifecycle
147        ),
148        data: None,
149        files: Vec::new(),
150    });
151
152    let handles_data = check_for_pattern(project_path, &["DataFrame", "Dataset", "DataLoader"]);
153    item = apply_check_outcome(
154        item,
155        &[
156            (has_inventory && has_classification && has_lifecycle, None),
157            (
158                has_inventory || has_classification,
159                Some("Partial data inventory (missing lifecycle or classification)"),
160            ),
161            (!handles_data, None),
162            (true, Some("No data inventory documentation")),
163        ],
164    );
165
166    item.finish_timed(start)
167}
168
169/// SDG-03: Privacy-Preserving Computation
170///
171/// **Claim:** Differential privacy correctly implemented where specified.
172///
173/// **Rejection Criteria (Major):**
174/// - ε-δ guarantees not met, composition budget exceeded
175pub fn check_privacy_preserving_computation(project_path: &Path) -> CheckItem {
176    let start = Instant::now();
177    let mut item = CheckItem::new(
178        "SDG-03",
179        "Privacy-Preserving Computation",
180        "Differential privacy correctly implemented",
181    )
182    .with_severity(Severity::Major)
183    .with_tps("Poka-Yoke — privacy by design");
184
185    // Check for differential privacy implementation
186    let has_dp = check_for_pattern(
187        project_path,
188        &[
189            "differential_privacy",
190            "epsilon",
191            "privacy_budget",
192            "laplace_noise",
193            "gaussian_mechanism",
194        ],
195    );
196
197    // Check for privacy budget tracking
198    let has_budget_tracking =
199        check_for_pattern(project_path, &["privacy_accountant", "budget_consumed", "composition"]);
200
201    // Check for privacy tests
202    let has_privacy_tests = check_for_test_pattern(project_path, &["privacy", "dp_test"]);
203
204    item = item.with_evidence(Evidence {
205        evidence_type: EvidenceType::StaticAnalysis,
206        description: format!(
207            "Privacy: dp_impl={}, budget_tracking={}, tests={}",
208            has_dp, has_budget_tracking, has_privacy_tests
209        ),
210        data: None,
211        files: Vec::new(),
212    });
213
214    let needs_privacy = check_for_pattern(project_path, &["pii", "personal_data", "gdpr"]);
215    item = apply_check_outcome(
216        item,
217        &[
218            (has_dp && has_budget_tracking && has_privacy_tests, None),
219            (has_dp, Some("DP implemented but missing budget tracking or tests")),
220            (!needs_privacy, None),
221            (true, Some("PII handling without differential privacy")),
222        ],
223    );
224
225    item.finish_timed(start)
226}
227
228/// SDG-04: Federated Learning Client Isolation
229///
230/// **Claim:** Federated learning clients send only model updates, never raw data.
231///
232/// **Rejection Criteria (Critical):**
233/// - Any payload containing raw training samples in network trace
234pub fn check_federated_learning_isolation(project_path: &Path) -> CheckItem {
235    let start = Instant::now();
236    let mut item = CheckItem::new(
237        "SDG-04",
238        "Federated Learning Client Isolation",
239        "FL clients send only model updates, never raw data",
240    )
241    .with_severity(Severity::Critical)
242    .with_tps("Jidoka — client-side enforcement");
243
244    // Check for federated learning implementation
245    let has_fl = check_for_pattern(
246        project_path,
247        &["federated", "FederatedClient", "model_update", "gradient_only"],
248    );
249
250    // Check for secure aggregation
251    let has_secure_agg = check_for_pattern(
252        project_path,
253        &["secure_aggregation", "SecureAggregator", "encrypted_gradient"],
254    );
255
256    // Check for data isolation tests
257    let has_isolation_tests =
258        check_for_test_pattern(project_path, &["client_isolation", "no_raw_data"]);
259
260    item = item.with_evidence(Evidence {
261        evidence_type: EvidenceType::StaticAnalysis,
262        description: format!(
263            "FL isolation: fl_impl={}, secure_agg={}, tests={}",
264            has_fl, has_secure_agg, has_isolation_tests
265        ),
266        data: None,
267        files: Vec::new(),
268    });
269
270    item = apply_check_outcome(
271        item,
272        &[
273            (!has_fl, None),
274            (has_fl && has_secure_agg && has_isolation_tests, None),
275            (
276                has_fl && has_secure_agg,
277                Some("FL with secure aggregation (missing isolation tests)"),
278            ),
279            (true, Some("FL without secure aggregation")),
280        ],
281    );
282
283    item.finish_timed(start)
284}
285
286/// SDG-05: Supply Chain Provenance (AI BOM)
287///
288/// **Claim:** All model weights and training data have verified provenance.
289///
290/// **Rejection Criteria (Critical):**
291/// - Pre-trained weight without signature, training data without chain-of-custody
292pub fn check_supply_chain_provenance(project_path: &Path) -> CheckItem {
293    let start = Instant::now();
294    let mut item = CheckItem::new(
295        "SDG-05",
296        "Supply Chain Provenance (AI BOM)",
297        "All model weights and data have verified provenance",
298    )
299    .with_severity(Severity::Critical)
300    .with_tps("Jidoka — supply chain circuit breaker");
301
302    // Check for AI BOM / SBOM
303    let bom_files = [
304        project_path.join("ai-bom.json"),
305        project_path.join("sbom.json"),
306        project_path.join("bom.xml"),
307        project_path.join(".sbom/"),
308    ];
309
310    let has_bom = bom_files.iter().any(|p| p.exists());
311
312    // Check for cargo-audit / cargo-deny
313    let cargo_toml = project_path.join("Cargo.toml");
314    let has_audit = cargo_toml
315        .exists()
316        .then(|| std::fs::read_to_string(&cargo_toml).ok())
317        .flatten()
318        .map(|c| c.contains("cargo-deny") || c.contains("cargo-audit"))
319        .unwrap_or(false);
320
321    // Check deny.toml for license/source policies
322    let deny_toml = project_path.join("deny.toml");
323    let has_deny_config = deny_toml.exists();
324
325    // Check for signature verification
326    let has_signature_check = check_for_pattern(
327        project_path,
328        &["verify_signature", "Ed25519", "sign_model", "provenance"],
329    );
330
331    item = item.with_evidence(Evidence {
332        evidence_type: EvidenceType::StaticAnalysis,
333        description: format!(
334            "Supply chain: bom={}, audit={}, deny={}, signatures={}",
335            has_bom, has_audit, has_deny_config, has_signature_check
336        ),
337        data: None,
338        files: Vec::new(),
339    });
340
341    item = apply_check_outcome(
342        item,
343        &[
344            (has_deny_config && (has_bom || has_signature_check), None),
345            (has_deny_config || has_audit, Some("Dependency audit configured (missing AI BOM)")),
346            (true, Some("No supply chain verification configured")),
347        ],
348    );
349
350    item.finish_timed(start)
351}
352
353/// SDG-06: VPC Isolation Verification
354///
355/// **Claim:** Compute resources spin up only in compliant sovereign regions.
356///
357/// **Rejection Criteria (Major):**
358/// - Any resource in non-approved region
359pub fn check_vpc_isolation(project_path: &Path) -> CheckItem {
360    let start = Instant::now();
361    let mut item = CheckItem::new(
362        "SDG-06",
363        "VPC Isolation Verification",
364        "Compute only in compliant sovereign regions",
365    )
366    .with_severity(Severity::Major)
367    .with_tps("Genchi Genbutsu — verify physical location");
368
369    // Check for IaC with region constraints
370    let iac_files = [
371        project_path.join("terraform/"),
372        project_path.join("infrastructure/"),
373        project_path.join("pulumi/"),
374        project_path.join("cloudformation/"),
375    ];
376
377    let has_iac = iac_files.iter().any(|p| p.exists());
378
379    // Check for region policy
380    let has_region_policy = check_for_pattern(
381        project_path,
382        &["allowed_regions", "region_whitelist", "sovereign_region"],
383    );
384
385    item = item.with_evidence(Evidence {
386        evidence_type: EvidenceType::StaticAnalysis,
387        description: format!("VPC: iac={}, region_policy={}", has_iac, has_region_policy),
388        data: None,
389        files: Vec::new(),
390    });
391
392    item = apply_check_outcome(
393        item,
394        &[
395            (!has_iac, None),
396            (has_iac && has_region_policy, None),
397            (true, Some("IaC without explicit region constraints")),
398        ],
399    );
400
401    item.finish_timed(start)
402}
403
404/// SDG-07: Data Classification Enforcement
405///
406/// **Claim:** Code paths enforce data classification levels.
407///
408/// **Rejection Criteria (Major):**
409/// - Sovereign-classified data processed by Public-tier code path
410pub fn check_data_classification_enforcement(project_path: &Path) -> CheckItem {
411    let start = Instant::now();
412    let mut item = CheckItem::new(
413        "SDG-07",
414        "Data Classification Enforcement",
415        "Code paths enforce data classification levels",
416    )
417    .with_severity(Severity::Major)
418    .with_tps("Poka-Yoke — classification-based routing");
419
420    // Check for type-level classification
421    let has_type_classification = check_for_pattern(
422        project_path,
423        &["Classification", "DataTier", "SecurityLevel", "Sovereign<", "Confidential<"],
424    );
425
426    // Check for runtime enforcement
427    let has_runtime_check = check_for_pattern(
428        project_path,
429        &["check_classification", "enforce_tier", "validate_access_level"],
430    );
431
432    item = item.with_evidence(Evidence {
433        evidence_type: EvidenceType::StaticAnalysis,
434        description: format!(
435            "Classification: type_level={}, runtime={}",
436            has_type_classification, has_runtime_check
437        ),
438        data: None,
439        files: Vec::new(),
440    });
441
442    let handles_classified =
443        check_for_pattern(project_path, &["sovereign", "confidential", "restricted"]);
444    item = apply_check_outcome(
445        item,
446        &[
447            (has_type_classification && has_runtime_check, None),
448            (
449                has_type_classification || has_runtime_check,
450                Some("Partial classification enforcement"),
451            ),
452            (!handles_classified, None),
453            (true, Some("Classified data without enforcement")),
454        ],
455    );
456
457    item.finish_timed(start)
458}
459
460/// SDG-08: Consent and Purpose Limitation
461///
462/// **Claim:** Data usage matches documented consent scope.
463///
464/// **Rejection Criteria (Major):**
465/// - Any data processing without matching consent record
466pub fn check_consent_purpose_limitation(project_path: &Path) -> CheckItem {
467    let start = Instant::now();
468    let mut item = CheckItem::new(
469        "SDG-08",
470        "Consent and Purpose Limitation",
471        "Data usage matches documented consent scope",
472    )
473    .with_severity(Severity::Major)
474    .with_tps("Legal controls pillar");
475
476    // Check for consent management
477    let has_consent =
478        check_for_pattern(project_path, &["consent", "purpose_limitation", "data_usage_agreement"]);
479
480    // Check for purpose binding
481    let has_purpose_binding =
482        check_for_pattern(project_path, &["purpose_id", "usage_scope", "consent_scope"]);
483
484    item = item.with_evidence(Evidence {
485        evidence_type: EvidenceType::StaticAnalysis,
486        description: format!(
487            "Consent: management={}, purpose_binding={}",
488            has_consent, has_purpose_binding
489        ),
490        data: None,
491        files: Vec::new(),
492    });
493
494    let handles_user_data = check_for_pattern(project_path, &["user_data", "personal", "pii"]);
495    item = apply_check_outcome(
496        item,
497        &[
498            (!handles_user_data, None),
499            (has_consent && has_purpose_binding, None),
500            (has_consent, Some("Consent tracking without purpose binding")),
501            (true, Some("User data handling without consent management")),
502        ],
503    );
504
505    item.finish_timed(start)
506}
507
508/// SDG-09: Right to Erasure (RTBF) Compliance
509///
510/// **Claim:** Data deletion requests fully propagate through all storage.
511///
512/// **Rejection Criteria (Major):**
513/// - Any trace of erased identity in storage or model
514pub fn check_rtbf_compliance(project_path: &Path) -> CheckItem {
515    let start = Instant::now();
516    let mut item = CheckItem::new(
517        "SDG-09",
518        "Right to Erasure (RTBF) Compliance",
519        "Deletion requests propagate through all storage",
520    )
521    .with_severity(Severity::Major)
522    .with_tps("Muda — data inventory hygiene");
523
524    // Check for deletion cascade
525    let has_deletion = check_for_pattern(
526        project_path,
527        &["delete_user", "erasure", "rtbf", "forget_user", "cascade_delete"],
528    );
529
530    // Check for model unlearning
531    let has_unlearning =
532        check_for_pattern(project_path, &["unlearn", "model_forget", "data_removal"]);
533
534    item = item.with_evidence(Evidence {
535        evidence_type: EvidenceType::StaticAnalysis,
536        description: format!(
537            "RTBF: deletion_cascade={}, model_unlearning={}",
538            has_deletion, has_unlearning
539        ),
540        data: None,
541        files: Vec::new(),
542    });
543
544    let stores_user_data = check_for_pattern(project_path, &["user_store", "persist_user", "save"]);
545    item = apply_check_outcome(
546        item,
547        &[
548            (!stores_user_data, None),
549            (has_deletion && has_unlearning, None),
550            (has_deletion, Some("Deletion without model unlearning")),
551            (true, Some("Data persistence without erasure mechanism")),
552        ],
553    );
554
555    item.finish_timed(start)
556}
557
558/// SDG-10: Cross-Border Transfer Logging
559///
560/// **Claim:** All cross-border data transfers are logged and justified.
561///
562/// **Rejection Criteria (Major):**
563/// - Any cross-border transfer without audit log entry
564pub fn check_cross_border_logging(project_path: &Path) -> CheckItem {
565    let start = Instant::now();
566    let mut item = CheckItem::new(
567        "SDG-10",
568        "Cross-Border Transfer Logging",
569        "All cross-border transfers logged and justified",
570    )
571    .with_severity(Severity::Major)
572    .with_tps("Auditability requirement");
573
574    // Check for transfer logging
575    let has_transfer_log = check_for_pattern(
576        project_path,
577        &["transfer_log", "cross_border", "data_export", "international_transfer"],
578    );
579
580    // Check for legal basis documentation
581    let has_legal_basis = check_for_pattern(
582        project_path,
583        &["legal_basis", "transfer_agreement", "adequacy_decision"],
584    );
585
586    item = item.with_evidence(Evidence {
587        evidence_type: EvidenceType::StaticAnalysis,
588        description: format!(
589            "Cross-border: logging={}, legal_basis={}",
590            has_transfer_log, has_legal_basis
591        ),
592        data: None,
593        files: Vec::new(),
594    });
595
596    let does_transfers = has_network_code(project_path);
597    item = apply_check_outcome(
598        item,
599        &[
600            (!does_transfers, None),
601            (has_transfer_log && has_legal_basis, None),
602            (has_transfer_log, Some("Transfer logging without legal basis")),
603            (true, Some("Network operations without transfer logging")),
604        ],
605    );
606
607    item.finish_timed(start)
608}
609
610/// SDG-11: Model Weight Sovereignty
611///
612/// **Claim:** Model weights trained on sovereign data remain under sovereign control.
613///
614/// **Rejection Criteria (Critical):**
615/// - Model weights accessible from non-sovereign context
616pub fn check_model_weight_sovereignty(project_path: &Path) -> CheckItem {
617    let start = Instant::now();
618    let mut item = CheckItem::new(
619        "SDG-11",
620        "Model Weight Sovereignty",
621        "Weights from sovereign data remain under sovereign control",
622    )
623    .with_severity(Severity::Critical)
624    .with_tps("Jidoka — weight exfiltration prevention");
625
626    // Check for weight access control
627    let has_access_control = check_for_pattern(
628        project_path,
629        &["weight_access", "model_acl", "sovereign_model", "protected_weights"],
630    );
631
632    // Check for encryption
633    let has_encryption =
634        check_for_pattern(project_path, &["encrypt_weights", "sealed_model", "encrypted_model"]);
635
636    // Check for key management
637    let has_key_mgmt = check_for_pattern(project_path, &["key_management", "kms", "key_rotation"]);
638
639    item = item.with_evidence(Evidence {
640        evidence_type: EvidenceType::StaticAnalysis,
641        description: format!(
642            "Weight sovereignty: access_control={}, encryption={}, key_mgmt={}",
643            has_access_control, has_encryption, has_key_mgmt
644        ),
645        data: None,
646        files: Vec::new(),
647    });
648
649    let handles_weights =
650        check_for_pattern(project_path, &["model_weights", "load_model", "save_model"]);
651    item = apply_check_outcome(
652        item,
653        &[
654            (!handles_weights, None),
655            (has_access_control && has_encryption, None),
656            (has_access_control || has_encryption, Some("Partial weight protection")),
657            (true, Some("Model weights without sovereignty controls")),
658        ],
659    );
660
661    item.finish_timed(start)
662}
663
664/// SDG-12: Inference Result Classification
665///
666/// **Claim:** Inference outputs inherit classification from input data.
667///
668/// **Rejection Criteria (Major):**
669/// - Sovereign input produces unclassified output
670pub fn check_inference_classification(project_path: &Path) -> CheckItem {
671    let start = Instant::now();
672    let mut item = CheckItem::new(
673        "SDG-12",
674        "Inference Result Classification",
675        "Outputs inherit classification from inputs",
676    )
677    .with_severity(Severity::Major)
678    .with_tps("Poka-Yoke — automatic classification propagation");
679
680    // Check for classification inheritance
681    let has_inheritance = check_for_pattern(
682        project_path,
683        &["inherit_classification", "propagate_tier", "output_classification"],
684    );
685
686    // Check for output tagging
687    let has_output_tagging =
688        check_for_pattern(project_path, &["tag_output", "classify_result", "result_tier"]);
689
690    item = item.with_evidence(Evidence {
691        evidence_type: EvidenceType::StaticAnalysis,
692        description: format!(
693            "Inference classification: inheritance={}, tagging={}",
694            has_inheritance, has_output_tagging
695        ),
696        data: None,
697        files: Vec::new(),
698    });
699
700    let does_inference = check_for_pattern(project_path, &["inference", "predict", "infer"]);
701    item = apply_check_outcome(
702        item,
703        &[
704            (!does_inference, None),
705            (has_inheritance && has_output_tagging, None),
706            (has_inheritance || has_output_tagging, Some("Partial classification propagation")),
707            (true, Some("Inference without classification propagation")),
708        ],
709    );
710
711    item.finish_timed(start)
712}
713
714/// SDG-13: Audit Log Immutability
715///
716/// **Claim:** Audit logs cannot be modified or deleted.
717///
718/// **Rejection Criteria (Critical):**
719/// - Any successful modification of historical log entry
720pub fn check_audit_log_immutability(project_path: &Path) -> CheckItem {
721    let start = Instant::now();
722    let mut item = CheckItem::new(
723        "SDG-13",
724        "Audit Log Immutability",
725        "Audit logs cannot be modified or deleted",
726    )
727    .with_severity(Severity::Critical)
728    .with_tps("Governance layer integrity");
729
730    // Check for append-only logging
731    let has_append_only =
732        check_for_pattern(project_path, &["append_only", "immutable_log", "write_once"]);
733
734    // Check for cryptographic chaining
735    let has_chaining = check_for_pattern(
736        project_path,
737        &["merkle", "hash_chain", "log_signature", "tamper_evident"],
738    );
739
740    // Check for audit trail implementation
741    let has_audit_trail =
742        check_for_pattern(project_path, &["audit_trail", "audit_log", "AuditEntry"]);
743
744    item = item.with_evidence(Evidence {
745        evidence_type: EvidenceType::StaticAnalysis,
746        description: format!(
747            "Audit immutability: append_only={}, chaining={}, trail={}",
748            has_append_only, has_chaining, has_audit_trail
749        ),
750        data: None,
751        files: Vec::new(),
752    });
753
754    item = apply_check_outcome(
755        item,
756        &[
757            (has_audit_trail && has_chaining, None),
758            (has_audit_trail, Some("Audit trail without cryptographic verification")),
759            (true, Some("No immutable audit logging")),
760        ],
761    );
762
763    item.finish_timed(start)
764}
765
766/// SDG-14: Third-Party API Isolation
767///
768/// **Claim:** No third-party API calls during sovereign-tier operations.
769///
770/// **Rejection Criteria (Critical):**
771/// - Any outbound call to non-sovereign endpoint
772pub fn check_third_party_isolation(project_path: &Path) -> CheckItem {
773    let start = Instant::now();
774    let mut item = CheckItem::new(
775        "SDG-14",
776        "Third-Party API Isolation",
777        "No third-party API calls during sovereign operations",
778    )
779    .with_severity(Severity::Critical)
780    .with_tps("Jidoka — network isolation");
781
782    // Check for network allowlist
783    let has_allowlist = check_for_pattern(
784        project_path,
785        &["allowlist", "whitelist", "approved_endpoints", "sovereign_endpoints"],
786    );
787
788    // Check for offline mode
789    let has_offline_mode =
790        check_for_pattern(project_path, &["offline_mode", "airgap", "no_network", "local_only"]);
791
792    // Check for network guards
793    let has_network_guard =
794        check_for_pattern(project_path, &["network_guard", "egress_filter", "outbound_check"]);
795
796    item = item.with_evidence(Evidence {
797        evidence_type: EvidenceType::StaticAnalysis,
798        description: format!(
799            "Third-party isolation: allowlist={}, offline={}, guard={}",
800            has_allowlist, has_offline_mode, has_network_guard
801        ),
802        data: None,
803        files: Vec::new(),
804    });
805
806    let does_network = has_network_code(project_path);
807    item = apply_check_outcome(
808        item,
809        &[
810            (!does_network || has_offline_mode, None),
811            (has_allowlist && has_network_guard, None),
812            (has_allowlist, Some("Allowlist without runtime guard")),
813            (true, Some("Network operations without isolation controls")),
814        ],
815    );
816
817    item.finish_timed(start)
818}
819
820/// SDG-15: Homomorphic/Secure Computation Verification
821///
822/// **Claim:** Secure computation primitives correctly implemented.
823///
824/// **Rejection Criteria (Major):**
825/// - Any information leakage beyond specified bounds
826pub fn check_secure_computation(project_path: &Path) -> CheckItem {
827    let start = Instant::now();
828    let mut item = CheckItem::new(
829        "SDG-15",
830        "Secure Computation Verification",
831        "Cryptographic primitives correctly implemented",
832    )
833    .with_severity(Severity::Major)
834    .with_tps("Formal verification requirement");
835
836    // Check for homomorphic encryption
837    let has_he = check_for_pattern(project_path, &["homomorphic", "fhe", "seal", "paillier"]);
838
839    // Check for MPC
840    let has_mpc = check_for_pattern(project_path, &["mpc", "secure_multiparty", "secret_sharing"]);
841
842    // Check for TEE
843    let has_tee = check_for_pattern(project_path, &["sgx", "enclave", "trusted_execution", "tee"]);
844
845    // Check for crypto tests
846    let has_crypto_tests =
847        check_for_test_pattern(project_path, &["crypto", "encryption", "secure_compute"]);
848
849    item = item.with_evidence(Evidence {
850        evidence_type: EvidenceType::StaticAnalysis,
851        description: format!(
852            "Secure compute: he={}, mpc={}, tee={}, tests={}",
853            has_he, has_mpc, has_tee, has_crypto_tests
854        ),
855        data: None,
856        files: Vec::new(),
857    });
858
859    let needs_secure_compute = has_he || has_mpc || has_tee;
860    item = apply_check_outcome(
861        item,
862        &[
863            (!needs_secure_compute, None),
864            (needs_secure_compute && has_crypto_tests, None),
865            (true, Some("Secure computation without comprehensive tests")),
866        ],
867    );
868
869    item.finish_timed(start)
870}
871
872// ============================================================================
873// Helper Functions
874// ============================================================================
875
876/// Apply the first matching outcome to a CheckItem.
877///
878/// Each entry is `(condition, outcome)` where `None` means pass and
879/// `Some(msg)` means partial with that message.  The first `true` condition
880/// wins; if nothing matches the item is returned unchanged.
881fn apply_check_outcome(item: CheckItem, checks: &[(bool, Option<&str>)]) -> CheckItem {
882    for &(condition, msg) in checks {
883        if condition {
884            return match msg {
885                Some(m) => item.partial(m),
886                None => item.pass(),
887            };
888        }
889    }
890    item
891}
892
893/// Check if any Rust source or config file contains any of the given patterns.
894fn check_for_pattern(project_path: &Path, patterns: &[&str]) -> bool {
895    super::helpers::source_or_config_contains_pattern(project_path, patterns)
896}
897
898/// Check if any test file contains any of the given patterns.
899fn check_for_test_pattern(project_path: &Path, patterns: &[&str]) -> bool {
900    super::helpers::test_contains_pattern(project_path, patterns)
901}
902
903/// Check if project has network code (HTTP, gRPC, etc.).
904fn has_network_code(project_path: &Path) -> bool {
905    check_for_pattern(
906        project_path,
907        &["reqwest", "hyper", "tonic", "TcpStream", "HttpClient", "fetch", "grpc"],
908    )
909}
910
911// ============================================================================
912// Tests
913// ============================================================================
914
915#[cfg(test)]
916mod tests {
917    use super::*;
918    use std::path::PathBuf;
919
920    #[test]
921    fn test_evaluate_all_returns_15_items() {
922        let path = PathBuf::from(".");
923        let items = evaluate_all(&path);
924        assert_eq!(items.len(), 15);
925    }
926
927    #[test]
928    fn test_all_items_have_tps_principle() {
929        let path = PathBuf::from(".");
930        let items = evaluate_all(&path);
931        for item in items {
932            assert!(!item.tps_principle.is_empty(), "Item {} missing TPS principle", item.id);
933        }
934    }
935
936    #[test]
937    fn test_all_items_have_evidence() {
938        let path = PathBuf::from(".");
939        let items = evaluate_all(&path);
940        for item in items {
941            assert!(!item.evidence.is_empty(), "Item {} missing evidence", item.id);
942        }
943    }
944
945    #[test]
946    fn test_sdg_01_data_residency() {
947        let path = PathBuf::from(".");
948        let item = check_data_residency_boundary(&path);
949        assert_eq!(item.id, "SDG-01");
950        assert_eq!(item.severity, Severity::Critical);
951    }
952
953    #[test]
954    fn test_sdg_05_supply_chain() {
955        let path = PathBuf::from(".");
956        let item = check_supply_chain_provenance(&path);
957        assert_eq!(item.id, "SDG-05");
958        assert_eq!(item.severity, Severity::Critical);
959    }
960
961    #[test]
962    fn test_sdg_13_audit_log() {
963        let path = PathBuf::from(".");
964        let item = check_audit_log_immutability(&path);
965        assert_eq!(item.id, "SDG-13");
966        assert_eq!(item.severity, Severity::Critical);
967    }
968
969    // =========================================================================
970    // Additional Coverage Tests
971    // =========================================================================
972
973    #[test]
974    fn test_sdg_02_data_inventory() {
975        let path = PathBuf::from(".");
976        let item = check_data_inventory_completeness(&path);
977        assert_eq!(item.id, "SDG-02");
978    }
979
980    #[test]
981    fn test_sdg_03_privacy_preserving() {
982        let path = PathBuf::from(".");
983        let item = check_privacy_preserving_computation(&path);
984        assert_eq!(item.id, "SDG-03");
985    }
986
987    #[test]
988    fn test_sdg_04_federated_learning() {
989        let path = PathBuf::from(".");
990        let item = check_federated_learning_isolation(&path);
991        assert_eq!(item.id, "SDG-04");
992    }
993
994    #[test]
995    fn test_sdg_06_vpc_isolation() {
996        let path = PathBuf::from(".");
997        let item = check_vpc_isolation(&path);
998        assert_eq!(item.id, "SDG-06");
999    }
1000
1001    #[test]
1002    fn test_sdg_07_data_classification() {
1003        let path = PathBuf::from(".");
1004        let item = check_data_classification_enforcement(&path);
1005        assert_eq!(item.id, "SDG-07");
1006    }
1007
1008    #[test]
1009    fn test_sdg_08_consent_purpose() {
1010        let path = PathBuf::from(".");
1011        let item = check_consent_purpose_limitation(&path);
1012        assert_eq!(item.id, "SDG-08");
1013    }
1014
1015    #[test]
1016    fn test_sdg_09_rtbf_compliance() {
1017        let path = PathBuf::from(".");
1018        let item = check_rtbf_compliance(&path);
1019        assert_eq!(item.id, "SDG-09");
1020    }
1021
1022    #[test]
1023    fn test_sdg_10_cross_border() {
1024        let path = PathBuf::from(".");
1025        let item = check_cross_border_logging(&path);
1026        assert_eq!(item.id, "SDG-10");
1027    }
1028
1029    #[test]
1030    fn test_sdg_11_model_weight() {
1031        let path = PathBuf::from(".");
1032        let item = check_model_weight_sovereignty(&path);
1033        assert_eq!(item.id, "SDG-11");
1034    }
1035
1036    #[test]
1037    fn test_sdg_12_inference_classification() {
1038        let path = PathBuf::from(".");
1039        let item = check_inference_classification(&path);
1040        assert_eq!(item.id, "SDG-12");
1041    }
1042
1043    #[test]
1044    fn test_sdg_14_third_party() {
1045        let path = PathBuf::from(".");
1046        let item = check_third_party_isolation(&path);
1047        assert_eq!(item.id, "SDG-14");
1048    }
1049
1050    #[test]
1051    fn test_sdg_15_secure_computation() {
1052        let path = PathBuf::from(".");
1053        let item = check_secure_computation(&path);
1054        assert_eq!(item.id, "SDG-15");
1055    }
1056
1057    #[test]
1058    fn test_nonexistent_path() {
1059        let path = PathBuf::from("/nonexistent/path");
1060        let items = evaluate_all(&path);
1061        assert_eq!(items.len(), 15);
1062    }
1063
1064    #[test]
1065    fn test_temp_dir_with_privacy_lib() {
1066        let temp_dir = std::env::temp_dir().join("test_privacy_lib");
1067        let _ = std::fs::remove_dir_all(&temp_dir);
1068        std::fs::create_dir_all(&temp_dir).expect("mkdir failed");
1069
1070        std::fs::write(
1071            temp_dir.join("Cargo.toml"),
1072            r#"
1073[package]
1074name = "test"
1075version = "0.1.0"
1076
1077[dependencies]
1078differential-privacy = "0.1"
1079"#,
1080        )
1081        .expect("unexpected failure");
1082
1083        let item = check_privacy_preserving_computation(&temp_dir);
1084        assert_eq!(item.id, "SDG-03");
1085
1086        let _ = std::fs::remove_dir_all(&temp_dir);
1087    }
1088
1089    #[test]
1090    fn test_temp_dir_with_data_types() {
1091        let temp_dir = std::env::temp_dir().join("test_data_types");
1092        let _ = std::fs::remove_dir_all(&temp_dir);
1093        std::fs::create_dir_all(temp_dir.join("src")).expect("mkdir failed");
1094
1095        std::fs::write(
1096            temp_dir.join("src/lib.rs"),
1097            r#"
1098pub struct DataInventory {
1099    pub pii: bool,
1100    pub classification: String,
1101}
1102"#,
1103        )
1104        .expect("unexpected failure");
1105
1106        let item = check_data_inventory_completeness(&temp_dir);
1107        assert_eq!(item.id, "SDG-02");
1108
1109        let _ = std::fs::remove_dir_all(&temp_dir);
1110    }
1111
1112    #[test]
1113    fn test_temp_dir_with_audit_logging() {
1114        let temp_dir = std::env::temp_dir().join("test_audit_log");
1115        let _ = std::fs::remove_dir_all(&temp_dir);
1116        std::fs::create_dir_all(temp_dir.join("src")).expect("mkdir failed");
1117
1118        std::fs::write(
1119            temp_dir.join("src/lib.rs"),
1120            r#"
1121pub struct AuditLog {
1122    pub append_only: bool,
1123    pub immutable: bool,
1124}
1125"#,
1126        )
1127        .expect("unexpected failure");
1128
1129        let item = check_audit_log_immutability(&temp_dir);
1130        assert_eq!(item.id, "SDG-13");
1131
1132        let _ = std::fs::remove_dir_all(&temp_dir);
1133    }
1134
1135    #[test]
1136    fn test_all_have_severity() {
1137        let path = PathBuf::from(".");
1138        let items = evaluate_all(&path);
1139        for item in items {
1140            // All should have a severity set
1141            assert!(
1142                item.severity == Severity::Critical
1143                    || item.severity == Severity::Major
1144                    || item.severity == Severity::Minor
1145            );
1146        }
1147    }
1148}