From 7b8e3226a16488143d68c4009bcc825463ecec48 Mon Sep 17 00:00:00 2001 From: Mohammad Abir Abbas aka uknowwhoab1r <66947064+mdabir1203@users.noreply.github.com> Date: Tue, 21 Oct 2025 22:44:46 +0600 Subject: [PATCH] feat: add compliance automation outputs --- Cargo.lock | 1 + Cargo.toml | 2 +- src/cli.rs | 380 ++++++++++++++++++- src/compliance.rs | 935 ++++++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 1 + 5 files changed, 1317 insertions(+), 2 deletions(-) create mode 100644 src/compliance.rs diff --git a/Cargo.lock b/Cargo.lock index b78d290..7025fdf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -739,6 +739,7 @@ dependencies = [ "iana-time-zone", "js-sys", "num-traits", + "serde", "wasm-bindgen", "windows-link 0.2.1", ] diff --git a/Cargo.toml b/Cargo.toml index 2c2d93c..3c3a43b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -59,7 +59,7 @@ native-tls = "0.2.14" itertools = "0.14.0" lazy_static = "1.5.0" idna = "1.1.0" -chrono = "0.4.41" +chrono = { version = "0.4.41", features = ["serde"] } dialoguer = "0.12.0" once_cell = "1.21.3" papaya = "0.2.3" diff --git a/src/cli.rs b/src/cli.rs index 278a900..3c08a6a 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -7,6 +7,13 @@ use std::fs; use std::io::{self, Write}; use std::path::{Path, PathBuf}; +use crate::compliance::{ + AccessControl, AccessControlExport, ComplianceVisualizerData, Criticality, DeploymentNode, + EntropyFinding, EntropyMonitor, GeoFenceDecision, GeoFencePolicy, IncidentInput, + IncidentReport, IncidentReporter, MfaFactor, MfaPolicy, PrivacyController, PrivacySnapshot, + SandboxScanResult, SandboxScanner, SbomComponent, TrustGraph, TrustGraphExport, + TrustGraphSummary, TrustRelation, TrustService, +}; use crate::{run, Args, BoxError}; // ============================================================================ @@ -706,6 +713,104 @@ impl ShadowMapCLI { Self::write_stub_file(&report_path, &report_payload)?; TerminalUI::print_success(&format!("Report saved: {}", report_path.display())); + // Regulatory & trust controls + let (geo_decision, geo_payload) = Self::render_geo_fence_stub(); + let geo_path = output_dir.join("geo-fence.json"); + Self::write_stub_file(&geo_path, &geo_payload)?; + TerminalUI::print_success(&format!( + "Geo-fence policy exported: {}", + geo_path.display() + )); + + let (tamper_finding, tamper_payload) = Self::render_entropy_monitor_stub(); + let tamper_path = output_dir.join("api-tamper-monitoring.json"); + Self::write_stub_file(&tamper_path, &tamper_payload)?; + TerminalUI::print_success(&format!( + "Entropy monitor baseline saved: {}", + tamper_path.display() + )); + TerminalUI::print_info(&format!( + "Entropy delta {:.2} • tamper suspected: {}", + tamper_finding.delta, + if tamper_finding.tamper_suspected { + "yes" + } else { + "no" + } + )); + + let (sandbox_result, sandbox_payload) = Self::render_sandbox_stub(); + let sandbox_path = output_dir.join("sandbox-scan.json"); + Self::write_stub_file(&sandbox_path, &sandbox_payload)?; + TerminalUI::print_success(&format!( + "Sandbox verification archived: {}", + sandbox_path.display() + )); + + let (trust_export, trust_summary) = Self::build_trust_graph_stub(); + let trust_path = output_dir.join("trust-graph.json"); + let trust_payload = + serde_json::to_string_pretty(&trust_export).unwrap_or_else(|_| "{}".to_string()); + Self::write_stub_file(&trust_path, &trust_payload)?; + TerminalUI::print_success(&format!("Trust graph generated: {}", trust_path.display())); + + let trust_summary_path = output_dir.join("trust-graph-summary.json"); + let trust_summary_payload = + serde_json::to_string_pretty(&trust_summary).unwrap_or_else(|_| "{}".to_string()); + Self::write_stub_file(&trust_summary_path, &trust_summary_payload)?; + TerminalUI::print_info(&format!( + "Trust graph summary: {}", + trust_summary_path.display() + )); + + let (privacy_snapshot, privacy_payload) = Self::render_privacy_stub(); + let privacy_path = output_dir.join("privacy-controls.json"); + Self::write_stub_file(&privacy_path, &privacy_payload)?; + TerminalUI::print_success(&format!( + "Privacy journal produced: {}", + privacy_path.display() + )); + + let (incident_report, incident_payload) = Self::render_incident_stub(); + let incident_path = output_dir.join("incident-workflow.json"); + Self::write_stub_file(&incident_path, &incident_payload)?; + TerminalUI::print_success(&format!( + "Incident workflow templated: {}", + incident_path.display() + )); + + let (access_export, access_payload) = Self::render_access_policy_stub(); + let access_path = output_dir.join("access-policy.json"); + Self::write_stub_file(&access_path, &access_payload)?; + TerminalUI::print_success(&format!( + "Access control map saved: {}", + access_path.display() + )); + + let visualizer_payload = + Self::render_visualizer_stub(&geo_decision, &trust_summary, &incident_report); + let visualizer_path = output_dir.join("compliance-visualizer.json"); + Self::write_stub_file(&visualizer_path, &visualizer_payload)?; + TerminalUI::print_info(&format!( + "Visualizer data prepared: {}", + visualizer_path.display() + )); + + let trust_service_payload = Self::render_trust_service_stub( + &trust_export, + &geo_decision, + &privacy_snapshot, + &incident_report, + &access_export, + Some(&sandbox_result), + ); + let trust_service_path = output_dir.join("trust-service.json"); + Self::write_stub_file(&trust_service_path, &trust_service_payload)?; + TerminalUI::print_success(&format!( + "Trust service bundle issued: {}", + trust_service_path.display() + )); + println!(); println!("{}", "═".repeat(70).bright_green()); println!( @@ -716,7 +821,12 @@ impl ShadowMapCLI { println!(); TerminalUI::print_info(&format!("📁 Output: {}/", output_dir.display())); - TerminalUI::print_info("📄 Files: sbom.json, scan-results.json, report.json"); + TerminalUI::print_info( + "📄 Files: sbom.json, scan-results.json, report.json, geo-fence.json, trust-graph.json", + ); + TerminalUI::print_info( + "📄 Extras: privacy-controls.json, incident-workflow.json, access-policy.json, trust-service.json", + ); Ok(()) } @@ -950,6 +1060,274 @@ impl ShadowMapCLI { } } + fn render_geo_fence_stub() -> (GeoFenceDecision, String) { + let nodes = vec![ + DeploymentNode::new( + "dubai-core-1", + "UAE", + "Dubai", + ["ISO 27001", "NESA"], + ["NCA National Cloud Framework", "GDPR"], + ["sovereign-key", "geo-fenced"], + 14, + ), + DeploymentNode::new( + "riyadh-edge-2", + "KSA", + "Riyadh", + ["ISO 27701"], + ["NCA National Cloud Framework"], + ["geo-fenced"], + 28, + ), + DeploymentNode::new( + "frankfurt-eu-1", + "EU", + "Frankfurt", + ["ISO 27017"], + ["GDPR"], + ["geo-fenced"], + 42, + ), + ]; + + let policy = GeoFencePolicy::new(["UAE", "EU"], ["NCA National Cloud Framework", "GDPR"]) + .with_required_controls(["sovereign-key", "geo-fenced"]); + let decision = policy.enforce(&nodes); + let payload = serde_json::to_string_pretty(&decision).unwrap_or_else(|_| "{}".to_string()); + (decision, payload) + } + + fn render_entropy_monitor_stub() -> (EntropyFinding, String) { + let baseline = EntropyMonitor::baseline_from( + b"GET /api/v1/health HTTP/1.1\r\nHost: api.shadowmap.ai\r\n\r\n", + ); + let mut monitor = EntropyMonitor::new(baseline, 0.75); + let suspicious_payload = b"POST /api/v1/config HTTP/2\r\nHost: api.shadowmap.ai\r\nX-Trace: e7d12af0aa88\r\n\r\n\x7f\x8e\xa3\xf1\x9b\x1d\x02\xff\xee\xdb"; + let finding = monitor.analyze(suspicious_payload); + let payload = serde_json::to_string_pretty(&finding).unwrap_or_else(|_| "{}".to_string()); + (finding, payload) + } + + fn render_sandbox_stub() -> (SandboxScanResult, String) { + let baseline = + EntropyMonitor::baseline_from(b"GET / HTTP/1.1\r\nHost: sandbox.shadowmap.ai\r\n\r\n"); + let mut monitor = EntropyMonitor::new(baseline, 0.6); + let scanner = SandboxScanner::new("shadowmap-rust-sandbox"); + let payload: Vec = vec![ + 0x7f, 0x45, 0x4c, 0x46, 0x02, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, + 0x3e, 0x00, 0x01, 0x00, 0x00, 0x00, 0x78, 0x56, 0x34, 0x12, + ]; + let result = scanner.scan(&payload, &mut monitor); + let payload = serde_json::to_string_pretty(&result).unwrap_or_else(|_| "{}".to_string()); + (result, payload) + } + + fn build_trust_graph_stub() -> (TrustGraphExport, TrustGraphSummary) { + let components = vec![ + SbomComponent::new( + "shadowmap-core", + VERSION, + "ShadowMap", + Criticality::Core, + "sha256:core", + ["Apache-2.0"], + 0.98, + ), + SbomComponent::new( + "openssl", + "3.2.1", + "OpenSSL Foundation", + Criticality::Core, + "sha256:openssl", + ["Apache-2.0"], + 0.82, + ), + SbomComponent::new( + "tokio", + "1.40.0", + "Tokio Project", + Criticality::Core, + "sha256:tokio", + ["MIT"], + 0.88, + ), + SbomComponent::new( + "slint", + "1.5.0", + "Slint Labs", + Criticality::Supporting, + "sha256:slint", + ["GPL-3.0", "Commercial"], + 0.73, + ), + ]; + + let mut graph = TrustGraph::from_components(components); + graph.relate( + "shadowmap-core", + "openssl", + TrustRelation::Runtime, + 0.21, + "TLS stack", + ); + graph.relate( + "shadowmap-core", + "tokio", + TrustRelation::Runtime, + 0.18, + "Async runtime", + ); + graph.relate( + "shadowmap-core", + "slint", + TrustRelation::Optional, + 0.35, + "Dashboard visualizer", + ); + + let export = graph.export(); + let summary = export.summary.clone(); + (export, summary) + } + + fn render_privacy_stub() -> (PrivacySnapshot, String) { + let mut controller = + PrivacyController::new(["subject_id", "purpose", "expires_at", "lawful_basis"]); + let record = json!({ + "subject_id": "uae-44321", + "email": "analyst@shadowmap.ai", + "purpose": "threat-intel", + "expires_at": "2025-01-01", + "lawful_basis": "legitimate_interest", + "raw_payload": "social graph correlations", + }); + let minimized = controller.minimize_record(&record); + controller.request_erasure("uae-44321"); + let snapshot = controller.snapshot(minimized); + let payload = serde_json::to_string_pretty(&snapshot).unwrap_or_else(|_| "{}".to_string()); + (snapshot, payload) + } + + fn render_incident_stub() -> (IncidentReport, String) { + let reporter = + IncidentReporter::new("soc@shadowmap.ai", ["NCA", "DEWA", "GDPR", "NIS2", "ENISA"]); + let incident = IncidentInput { + incident_id: "INC-2024-0925".to_string(), + severity: "high".to_string(), + detected_at: Utc::now(), + description: "Entropy deviation detected on supply chain webhook".to_string(), + impacted_assets: vec!["api.shadowmap.ai".to_string(), "audit-bus".to_string()], + containment_actions: vec![ + "revoked api token".to_string(), + "isolated geo-fence segment".to_string(), + ], + status: "contained".to_string(), + }; + let report = reporter.create_report(incident); + let payload = serde_json::to_string_pretty(&report).unwrap_or_else(|_| "{}".to_string()); + (report, payload) + } + + fn render_access_policy_stub() -> (AccessControlExport, String) { + let mut access = AccessControl::new(MfaPolicy { + min_validated: 2, + allowed_methods: vec![ + "totp".to_string(), + "webauthn".to_string(), + "hardware-key".to_string(), + ], + }); + access.define_role("analyst", ["view:reports", "download:evidence"]); + access.define_role( + "admin", + [ + "view:reports", + "download:evidence", + "manage:users", + "configure:policies", + ], + ); + access.define_role("auditor", ["view:reports", "request:evidence"]); + access.assign("amira.al-farsi", "analyst"); + access.assign("yousef.al-nuaimi", "admin"); + access.assign("salma.hassan", "auditor"); + + let evaluations = vec![ + access.evaluate( + "amira.al-farsi", + "download:evidence", + &[ + MfaFactor { + method: "totp".to_string(), + validated: true, + }, + MfaFactor { + method: "webauthn".to_string(), + validated: true, + }, + ], + ), + access.evaluate( + "yousef.al-nuaimi", + "configure:policies", + &[ + MfaFactor { + method: "totp".to_string(), + validated: true, + }, + MfaFactor { + method: "hardware-key".to_string(), + validated: true, + }, + ], + ), + access.evaluate( + "salma.hassan", + "download:evidence", + &[MfaFactor { + method: "totp".to_string(), + validated: true, + }], + ), + ]; + + let export = access.export_model(&evaluations); + let payload = serde_json::to_string_pretty(&export).unwrap_or_else(|_| "{}".to_string()); + (export, payload) + } + + fn render_visualizer_stub( + geo: &GeoFenceDecision, + trust: &TrustGraphSummary, + incident: &IncidentReport, + ) -> String { + let visualizer = ComplianceVisualizerData::from_context(geo, trust, incident); + serde_json::to_string_pretty(&visualizer).unwrap_or_else(|_| "{}".to_string()) + } + + fn render_trust_service_stub( + trust_export: &TrustGraphExport, + geo: &GeoFenceDecision, + privacy: &PrivacySnapshot, + incident: &IncidentReport, + access: &AccessControlExport, + sandbox: Option<&SandboxScanResult>, + ) -> String { + let mut service = TrustService::new( + trust_export.clone(), + geo.clone(), + privacy.clone(), + incident.clone(), + access.clone(), + ); + if let Some(sandbox_result) = sandbox { + service = service.with_sandbox(sandbox_result.clone()); + } + let payload = service.export(); + serde_json::to_string_pretty(&payload).unwrap_or_else(|_| "{}".to_string()) + } + fn write_stub_file(path: &Path, contents: &str) -> Result<(), BoxError> { if let Some(parent) = path.parent() { if !parent.as_os_str().is_empty() { diff --git a/src/compliance.rs b/src/compliance.rs new file mode 100644 index 0000000..64525cb --- /dev/null +++ b/src/compliance.rs @@ -0,0 +1,935 @@ +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; +use std::collections::{BTreeSet, HashMap, HashSet}; +use std::time::Instant; + +pub const NATIONAL_FRAMEWORKS: &[&str] = &[ + "NCA National Cloud Framework", + "DEWA Digital Infrastructure Policy", + "UAE AI Strategy 2031", +]; + +pub const GLOBAL_FRAMEWORKS: &[&str] = &["GDPR", "NIS2", "ENISA"]; + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)] +pub struct DeploymentNode { + pub identifier: String, + pub region: String, + pub jurisdiction: String, + pub certifications: BTreeSet, + pub residency_scope: BTreeSet, + pub controls: BTreeSet, + pub latency_ms: u32, +} + +impl DeploymentNode { + pub fn new( + identifier: impl Into, + region: impl Into, + jurisdiction: impl Into, + certifications: impl IntoIterator>, + residency_scope: impl IntoIterator>, + controls: impl IntoIterator>, + latency_ms: u32, + ) -> Self { + Self { + identifier: identifier.into(), + region: region.into(), + jurisdiction: jurisdiction.into(), + certifications: certifications.into_iter().map(Into::into).collect(), + residency_scope: residency_scope.into_iter().map(Into::into).collect(), + controls: controls.into_iter().map(Into::into).collect(), + latency_ms, + } + } +} + +#[derive(Clone, Debug, Serialize)] +pub struct GeoFenceDecision { + pub compliant_nodes: Vec, + pub quarantined_nodes: Vec, + pub alignment: Vec, + pub generated_at: DateTime, + pub notes: Vec, +} + +impl GeoFenceDecision { + pub fn empty() -> Self { + Self { + compliant_nodes: Vec::new(), + quarantined_nodes: Vec::new(), + alignment: Vec::new(), + generated_at: Utc::now(), + notes: Vec::new(), + } + } +} + +pub struct GeoFencePolicy { + allowed_regions: HashSet, + required_controls: HashSet, + residency_frameworks: HashSet, +} + +impl GeoFencePolicy { + pub fn new( + regions: impl IntoIterator>, + frameworks: impl IntoIterator>, + ) -> Self { + Self { + allowed_regions: regions + .into_iter() + .map(|s| s.as_ref().to_string()) + .collect(), + required_controls: HashSet::new(), + residency_frameworks: frameworks + .into_iter() + .map(|s| s.as_ref().to_string()) + .collect(), + } + } + + pub fn with_required_controls( + mut self, + controls: impl IntoIterator>, + ) -> Self { + self.required_controls = controls + .into_iter() + .map(|s| s.as_ref().to_string()) + .collect(); + self + } + + pub fn enforce(&self, nodes: &[DeploymentNode]) -> GeoFenceDecision { + let mut compliant = Vec::new(); + let mut quarantined = Vec::new(); + let mut notes = Vec::new(); + + for node in nodes { + let region_allowed = self.allowed_regions.contains(&node.region); + let controls_present = self + .required_controls + .iter() + .all(|control| node.controls.contains(control)); + let residency_ok = node + .residency_scope + .iter() + .any(|scope| self.residency_frameworks.contains(scope)); + + if region_allowed && controls_present && residency_ok { + compliant.push(node.clone()); + } else { + let mut reason = Vec::new(); + if !region_allowed { + reason.push(format!("region {} not authorised", node.region)); + } + if !controls_present { + reason.push("missing sovereign controls".to_string()); + } + if !residency_ok { + reason.push("residency policy not aligned".to_string()); + } + notes.push(format!( + "{} quarantined: {}", + node.identifier, + reason.join(", ") + )); + quarantined.push(node.clone()); + } + } + + GeoFenceDecision { + compliant_nodes: compliant, + quarantined_nodes: quarantined, + alignment: self + .residency_frameworks + .iter() + .cloned() + .chain(NATIONAL_FRAMEWORKS.iter().map(|s| s.to_string())) + .collect(), + generated_at: Utc::now(), + notes, + } + } +} + +#[derive(Clone, Debug, Serialize)] +pub struct EntropyFinding { + pub observed_entropy: f64, + pub baseline_entropy: f64, + pub delta: f64, + pub tamper_suspected: bool, + pub sample_size: usize, + pub timestamp: DateTime, + pub frameworks: Vec, +} + +#[derive(Clone, Debug)] +pub struct EntropyMonitor { + baseline: f64, + tolerance: f64, + history: Vec, + max_history: usize, +} + +impl EntropyMonitor { + pub fn new(baseline: f64, tolerance: f64) -> Self { + Self { + baseline, + tolerance, + history: Vec::new(), + max_history: 24, + } + } + + pub fn baseline_from(payload: &[u8]) -> f64 { + Self::entropy(payload) + } + + pub fn analyze(&mut self, payload: &[u8]) -> EntropyFinding { + let entropy = Self::entropy(payload); + let delta = (entropy - self.baseline).abs(); + let tamper = delta > self.tolerance; + + self.history.push(entropy); + if self.history.len() > self.max_history { + self.history.remove(0); + } + + if !tamper { + let sum: f64 = self.history.iter().sum(); + self.baseline = sum / self.history.len() as f64; + } + + EntropyFinding { + observed_entropy: entropy, + baseline_entropy: self.baseline, + delta, + tamper_suspected: tamper, + sample_size: payload.len(), + timestamp: Utc::now(), + frameworks: vec![ + "Supply chain integrity".to_string(), + "Entropy guardrail".to_string(), + "GDPR Art.32".to_string(), + ], + } + } + + fn entropy(payload: &[u8]) -> f64 { + let mut counts = [0usize; 256]; + for byte in payload { + counts[*byte as usize] += 1; + } + let len = payload.len() as f64; + if len == 0.0 { + return 0.0; + } + + counts + .iter() + .filter(|count| **count > 0) + .map(|count| { + let p = *count as f64 / len; + -p * p.log2() + }) + .sum() + } +} + +#[derive(Clone, Debug, Serialize)] +#[serde(rename_all = "snake_case")] +pub enum Criticality { + Core, + Supporting, + Development, +} + +#[derive(Clone, Debug, Serialize)] +pub struct SbomComponent { + pub name: String, + pub version: String, + pub supplier: String, + pub criticality: Criticality, + pub integrity_hash: String, + pub licenses: Vec, + pub confidence: f32, +} + +impl SbomComponent { + pub fn new( + name: impl Into, + version: impl Into, + supplier: impl Into, + criticality: Criticality, + integrity_hash: impl Into, + licenses: impl IntoIterator>, + confidence: f32, + ) -> Self { + Self { + name: name.into(), + version: version.into(), + supplier: supplier.into(), + criticality, + integrity_hash: integrity_hash.into(), + licenses: licenses.into_iter().map(Into::into).collect(), + confidence, + } + } +} + +#[derive(Clone, Debug, Serialize)] +#[serde(rename_all = "snake_case")] +pub enum TrustRelation { + Runtime, + Build, + Optional, +} + +#[derive(Clone, Debug, Serialize)] +pub struct TrustEdge { + pub from: String, + pub to: String, + pub relation: TrustRelation, + pub risk: f32, + pub notes: String, +} + +#[derive(Clone, Debug, Default)] +pub struct TrustGraph { + components: HashMap, + edges: Vec, +} + +impl TrustGraph { + pub fn from_components(components: impl IntoIterator) -> Self { + let mut graph = Self::default(); + for component in components { + graph.components.insert(component.name.clone(), component); + } + graph + } + + pub fn relate( + &mut self, + source: impl Into, + target: impl Into, + relation: TrustRelation, + risk: f32, + notes: impl Into, + ) { + self.edges.push(TrustEdge { + from: source.into(), + to: target.into(), + relation, + risk, + notes: notes.into(), + }); + } + + pub fn summary(&self) -> TrustGraphSummary { + let edge_count = self.edges.len(); + let average_risk = if edge_count > 0 { + self.edges.iter().map(|edge| edge.risk).sum::() / edge_count as f32 + } else { + 0.0 + }; + + let highest = self + .edges + .iter() + .max_by(|a, b| { + a.risk + .partial_cmp(&b.risk) + .unwrap_or(std::cmp::Ordering::Equal) + }) + .map(|edge| format!("{} -> {} ({:.2})", edge.from, edge.to, edge.risk)); + + TrustGraphSummary { + nodes: self.components.len(), + edges: edge_count, + average_risk, + highest_risk: highest, + frameworks: [NATIONAL_FRAMEWORKS, GLOBAL_FRAMEWORKS] + .into_iter() + .flat_map(|items| items.iter().map(|s| s.to_string())) + .collect(), + } + } + + pub fn export(&self) -> TrustGraphExport { + TrustGraphExport { + components: self.components.values().cloned().collect::>(), + edges: self.edges.clone(), + summary: self.summary(), + } + } +} + +#[derive(Clone, Debug, Serialize)] +pub struct TrustGraphSummary { + pub nodes: usize, + pub edges: usize, + pub average_risk: f32, + pub highest_risk: Option, + pub frameworks: Vec, +} + +#[derive(Clone, Debug, Serialize)] +pub struct TrustGraphExport { + pub components: Vec, + pub edges: Vec, + pub summary: TrustGraphSummary, +} + +#[derive(Clone, Debug, Serialize)] +pub enum DeletionRequestStatus { + Pending, + Processed, +} + +#[derive(Clone, Debug, Serialize)] +pub struct DeletionRequest { + pub subject_id: String, + pub requested_at: DateTime, + pub status: DeletionRequestStatus, +} + +#[derive(Clone, Debug)] +pub struct PrivacyController { + allowed_fields: HashSet, + requests: HashMap, +} + +impl PrivacyController { + pub fn new(fields: impl IntoIterator>) -> Self { + Self { + allowed_fields: fields.into_iter().map(|f| f.as_ref().to_string()).collect(), + requests: HashMap::new(), + } + } + + pub fn minimize_record(&self, record: &serde_json::Value) -> serde_json::Value { + match record { + serde_json::Value::Object(map) => { + let mut filtered = serde_json::Map::new(); + for (key, value) in map { + if self.allowed_fields.contains(&key.to_string()) { + filtered.insert(key.clone(), value.clone()); + } + } + serde_json::Value::Object(filtered) + } + _ => serde_json::Value::Null, + } + } + + pub fn request_erasure(&mut self, subject_id: impl Into) -> DeletionRequest { + let id = subject_id.into(); + let request = DeletionRequest { + subject_id: id.clone(), + requested_at: Utc::now(), + status: DeletionRequestStatus::Pending, + }; + self.requests.insert(id.clone(), request.clone()); + request + } + + pub fn close_request(&mut self, subject_id: &str) -> Option { + if let Some(request) = self.requests.get_mut(subject_id) { + request.status = DeletionRequestStatus::Processed; + Some(request.clone()) + } else { + None + } + } + + pub fn outstanding_requests(&self) -> Vec { + self.requests + .values() + .filter(|request| matches!(request.status, DeletionRequestStatus::Pending)) + .cloned() + .collect() + } + + pub fn snapshot(&self, minimized_record: serde_json::Value) -> PrivacySnapshot { + PrivacySnapshot { + minimized_example: minimized_record, + outstanding_requests: self.outstanding_requests(), + enforcement_controls: vec![ + "Data minimisation enforced".to_string(), + "Automated deletion workflow".to_string(), + "GDPR Art.17 compliance".to_string(), + ], + generated_at: Utc::now(), + } + } +} + +#[derive(Clone, Debug, Serialize)] +pub struct PrivacySnapshot { + pub minimized_example: serde_json::Value, + pub outstanding_requests: Vec, + pub enforcement_controls: Vec, + pub generated_at: DateTime, +} + +#[derive(Clone, Debug, Serialize)] +pub struct IncidentInput { + pub incident_id: String, + pub severity: String, + pub detected_at: DateTime, + pub description: String, + pub impacted_assets: Vec, + pub containment_actions: Vec, + pub status: String, +} + +#[derive(Clone, Debug, Serialize)] +pub struct IncidentReport { + pub incident: IncidentInput, + pub regulatory_mapping: HashMap, + pub notification_deadlines: HashMap, + pub contact: String, + pub generated_at: DateTime, +} + +pub struct IncidentReporter { + contact: String, + regulators: Vec, +} + +impl IncidentReporter { + pub fn new( + contact: impl Into, + regulators: impl IntoIterator>, + ) -> Self { + Self { + contact: contact.into(), + regulators: regulators + .into_iter() + .map(|s| s.as_ref().to_string()) + .collect(), + } + } + + pub fn create_report(&self, incident: IncidentInput) -> IncidentReport { + let mut regulatory_mapping = HashMap::new(); + regulatory_mapping.insert( + "NIS2".to_string(), + "Article 23 - 24 hour notification".to_string(), + ); + regulatory_mapping.insert("ENISA".to_string(), "EU CSIRT baseline sharing".to_string()); + regulatory_mapping.insert( + "GDPR".to_string(), + "Articles 33-34 personal data breach handling".to_string(), + ); + regulatory_mapping.insert( + "NCA".to_string(), + "Cloud sector controls reporting".to_string(), + ); + + for regulator in &self.regulators { + regulatory_mapping + .entry(regulator.clone()) + .or_insert_with(|| "Regulator aligned via automated workflow".to_string()); + } + + let mut deadlines = HashMap::new(); + deadlines.insert("NIS2".to_string(), "24h initial, 72h final".to_string()); + deadlines.insert("GDPR".to_string(), "72h supervisory authority".to_string()); + deadlines.insert( + "ENISA".to_string(), + "Real-time CSIRT coordination".to_string(), + ); + + IncidentReport { + incident, + regulatory_mapping, + notification_deadlines: deadlines, + contact: self.contact.clone(), + generated_at: Utc::now(), + } + } +} + +#[derive(Clone, Debug, Serialize)] +pub struct MfaPolicy { + pub min_validated: usize, + pub allowed_methods: Vec, +} + +#[derive(Clone, Debug, Serialize)] +pub struct MfaFactor { + pub method: String, + pub validated: bool, +} + +#[derive(Clone, Debug, Serialize)] +pub struct AccessDecisionRecord { + pub user: String, + pub permission: String, + pub granted: bool, + pub reason: String, + pub frameworks: Vec, +} + +#[derive(Clone, Debug)] +pub struct AccessControl { + role_policies: HashMap>, + user_roles: HashMap>, + policy: MfaPolicy, +} + +impl AccessControl { + pub fn new(policy: MfaPolicy) -> Self { + Self { + role_policies: HashMap::new(), + user_roles: HashMap::new(), + policy, + } + } + + pub fn define_role( + &mut self, + role: impl Into, + permissions: impl IntoIterator>, + ) { + let entry = self.role_policies.entry(role.into()).or_default(); + for perm in permissions { + entry.insert(perm.as_ref().to_string()); + } + } + + pub fn assign(&mut self, user: impl Into, role: impl Into) { + let user_entry = self.user_roles.entry(user.into()).or_default(); + user_entry.insert(role.into()); + } + + pub fn evaluate( + &self, + user: &str, + permission: &str, + factors: &[MfaFactor], + ) -> AccessDecisionRecord { + let validated = factors.iter().filter(|factor| factor.validated).count(); + if validated < self.policy.min_validated { + return AccessDecisionRecord { + user: user.to_string(), + permission: permission.to_string(), + granted: false, + reason: format!( + "{} factors validated, {} required", + validated, self.policy.min_validated + ), + frameworks: vec![ + "GDPR Art.32".to_string(), + "NIS2 Article 21".to_string(), + "ENISA IAM baseline".to_string(), + ], + }; + } + + let roles = match self.user_roles.get(user) { + Some(roles) => roles, + None => { + return AccessDecisionRecord { + user: user.to_string(), + permission: permission.to_string(), + granted: false, + reason: "no roles assigned".to_string(), + frameworks: vec!["Least privilege".to_string()], + } + } + }; + + let permitted = roles.iter().any(|role| { + self.role_policies + .get(role) + .map(|policies| policies.contains(permission)) + .unwrap_or(false) + }); + + if permitted { + AccessDecisionRecord { + user: user.to_string(), + permission: permission.to_string(), + granted: true, + reason: "role policy satisfied".to_string(), + frameworks: vec![ + "Zero trust enforced".to_string(), + "UAE AI Strategy assurance".to_string(), + ], + } + } else { + AccessDecisionRecord { + user: user.to_string(), + permission: permission.to_string(), + granted: false, + reason: "permission not granted by role".to_string(), + frameworks: vec![ + "Access denied".to_string(), + "Audit trail captured".to_string(), + ], + } + } + } + + pub fn export_model(&self, evaluations: &[AccessDecisionRecord]) -> AccessControlExport { + let roles = self + .role_policies + .iter() + .map(|(role, perms)| { + let mut list: Vec<_> = perms.iter().cloned().collect(); + list.sort(); + (role.clone(), list) + }) + .collect::>(); + + let assignments = self + .user_roles + .iter() + .map(|(user, roles)| { + let mut list: Vec<_> = roles.iter().cloned().collect(); + list.sort(); + (user.clone(), list) + }) + .collect::>(); + + AccessControlExport { + roles, + assignments, + evaluations: evaluations.to_vec(), + policy: self.policy.clone(), + } + } +} + +#[derive(Clone, Debug, Serialize)] +pub struct AccessControlExport { + pub roles: HashMap>, + pub assignments: HashMap>, + pub evaluations: Vec, + pub policy: MfaPolicy, +} + +#[derive(Clone, Debug, Serialize)] +pub struct FrameworkScore { + pub framework: String, + pub score: f32, + pub notes: String, +} + +#[derive(Clone, Debug, Serialize)] +pub struct ComplianceVisualizerData { + pub summary: String, + pub ai_reasoning: Vec, + pub frameworks: Vec, + pub geo_nodes: Vec, +} + +impl ComplianceVisualizerData { + pub fn from_context( + geo: &GeoFenceDecision, + trust: &TrustGraphSummary, + incident: &IncidentReport, + ) -> Self { + let summary = format!( + "{} compliant nodes • {:.2} average supply-chain risk", + geo.compliant_nodes.len(), + trust.average_risk + ); + + let mut ai_reasoning = Vec::new(); + ai_reasoning.push(format!( + "Geo-fence ensures {} regions align with {:?}", + geo.compliant_nodes.len(), + geo.alignment + )); + if let Some(highest) = &trust.highest_risk { + ai_reasoning.push(format!("Trust graph hotspot: {highest}")); + } + ai_reasoning.push(format!( + "Incident {} mapped to {} regulators", + incident.incident.incident_id, + incident.regulatory_mapping.len() + )); + + let frameworks = geo + .alignment + .iter() + .enumerate() + .map(|(index, framework)| FrameworkScore { + framework: framework.clone(), + score: 0.85 - (index as f32 * 0.03), + notes: "Residency guardrail enforced".to_string(), + }) + .chain( + trust + .frameworks + .iter() + .enumerate() + .map(|(index, framework)| FrameworkScore { + framework: framework.clone(), + score: 0.78 - (index as f32 * 0.01), + notes: "Supply chain mapping".to_string(), + }), + ) + .collect(); + + Self { + summary, + ai_reasoning, + frameworks, + geo_nodes: geo.compliant_nodes.clone(), + } + } +} + +#[derive(Clone, Debug, Serialize)] +pub struct SandboxFinding { + pub heuristic: String, + pub severity: String, + pub note: String, +} + +#[derive(Clone, Debug, Serialize)] +pub struct SandboxScanResult { + pub engine: String, + pub status: String, + pub findings: Vec, + pub tamper: Option, + pub frameworks: Vec, + pub duration_ms: u64, +} + +pub struct SandboxScanner { + engine: String, +} + +impl SandboxScanner { + pub fn new(engine: impl Into) -> Self { + Self { + engine: engine.into(), + } + } + + pub fn scan(&self, payload: &[u8], monitor: &mut EntropyMonitor) -> SandboxScanResult { + let start = Instant::now(); + let entropy = monitor.analyze(payload); + + let mut findings = Vec::new(); + let mut status = "clean".to_string(); + + if entropy.tamper_suspected { + findings.push(SandboxFinding { + heuristic: "entropy-anomaly".to_string(), + severity: "high".to_string(), + note: format!("delta {:.2} exceeds tolerance", entropy.delta), + }); + status = "quarantined".to_string(); + } + + let ascii_ratio = payload + .iter() + .filter(|byte| byte.is_ascii_alphanumeric()) + .count() as f32 + / payload.len().max(1) as f32; + if ascii_ratio < 0.25 { + findings.push(SandboxFinding { + heuristic: "low-ascii".to_string(), + severity: "medium".to_string(), + note: "Binary payload detected".to_string(), + }); + } + + if payload.windows(4).any(|window| window == b"\x7fELF") { + findings.push(SandboxFinding { + heuristic: "elf-header".to_string(), + severity: "critical".to_string(), + note: "Executable artefact staged".to_string(), + }); + status = "quarantined".to_string(); + } + + SandboxScanResult { + engine: self.engine.clone(), + status, + findings, + tamper: if entropy.tamper_suspected { + Some(entropy) + } else { + None + }, + frameworks: vec![ + "ENISA sandboxing".to_string(), + "NIS2 detection".to_string(), + "GDPR data minimisation".to_string(), + ], + duration_ms: start.elapsed().as_millis() as u64, + } + } +} + +#[derive(Clone, Debug, Serialize)] +pub struct TrustServicePayload { + pub issued_at: DateTime, + pub trust_graph: TrustGraphExport, + pub geo_fence: GeoFenceDecision, + pub privacy: PrivacySnapshot, + pub incident_template: IncidentReport, + pub access_model: AccessControlExport, + pub sandbox_scan: Option, + pub posture: Vec, +} + +pub struct TrustService { + trust_graph: TrustGraphExport, + geo_fence: GeoFenceDecision, + privacy: PrivacySnapshot, + incident: IncidentReport, + access: AccessControlExport, + sandbox: Option, +} + +impl TrustService { + pub fn new( + trust_graph: TrustGraphExport, + geo_fence: GeoFenceDecision, + privacy: PrivacySnapshot, + incident: IncidentReport, + access: AccessControlExport, + ) -> Self { + Self { + trust_graph, + geo_fence, + privacy, + incident, + access, + sandbox: None, + } + } + + pub fn with_sandbox(mut self, sandbox: SandboxScanResult) -> Self { + self.sandbox = Some(sandbox); + self + } + + pub fn export(&self) -> TrustServicePayload { + TrustServicePayload { + issued_at: Utc::now(), + trust_graph: self.trust_graph.clone(), + geo_fence: self.geo_fence.clone(), + privacy: self.privacy.clone(), + incident_template: self.incident.clone(), + access_model: self.access.clone(), + sandbox_scan: self.sandbox.clone(), + posture: vec![ + "NCA residency guardrails".to_string(), + "DEWA sovereign controls".to_string(), + "UAE AI Strategy assurance".to_string(), + "GDPR/NIS2/ENISA mapped".to_string(), + ], + } + } +} diff --git a/src/lib.rs b/src/lib.rs index d92ed65..f872a47 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,6 +2,7 @@ mod agent; pub mod args; pub mod cli; mod cloud; +pub mod compliance; mod constants; mod cors; mod dns;