//! Framework compliance enforcement proofs (GL #424, H3 Epic A — AC 1 + 2). //! //! Every `coverage = "full"` claim in `rust/data/compliance/mappings/*.toml` names a //! test in this file; `mapping_test_names_exist_in_this_file` fails the //! build if a claim points at a test that doesn't exist. Each test proves //! BOTH directions where meaningful: the mechanism enforces (reference //! pack / engine behavior) and a violation is detectable (weak pack ⇒ //! downgrade, tampered log ⇒ invalid chain, denied tool ⇒ event). use lean_ctx::core::compliance::{self, Coverage, RowStatus}; use lean_ctx::core::events::{EventKind, LeanCtxEvent}; use lean_ctx::core::ocp; use lean_ctx::core::policy::{ResolvedPolicy, builtin, resolve}; fn resolved(pack_name: &str) -> ResolvedPolicy { let pack = builtin::get(pack_name).unwrap_or_else(|| panic!("builtin pack {pack_name}")); resolve(&pack).expect("pack resolves") } fn row_status(framework: &str, control: &str, pack: &str) -> (RowStatus, String) { let mapping = compliance::get(framework).expect("framework exists"); let policy = resolved(pack); let report = compliance::report(mapping, Some(&policy)); let row = report .rows .into_iter() .find(|r| r.id == control) .unwrap_or_else(|| panic!("control {control} in {framework}")); (row.status, row.detail) } // ── meta: mapping ↔ test drift guard ───────────────────────────────────────── #[test] fn mapping_test_names_exist_in_this_file() { let source = include_str!("compliance_frameworks.rs"); for mapping in compliance::frameworks() { for control in &mapping.controls { if let Some(test) = &control.test { assert!( source.contains(&format!("fn {test}(")), "{}/{} names test '{test}' which does not exist in compliance_frameworks.rs", mapping.framework, control.id ); } } } } // ── AC 1: EU AI Act reference report ───────────────────────────────────────── #[test] fn eu_ai_act_reference_report_has_ten_plus_enforced_full_controls() { let mapping = compliance::get("eu-ai-act").expect("mapping"); let policy = resolved(&mapping.reference_pack); let report = compliance::report(mapping, Some(&policy)); let enforced_full = report .rows .iter() .filter(|r| { r.coverage == Coverage::Full && matches!(r.status, RowStatus::Enforced | RowStatus::EngineGuarantee) }) .count(); assert!( enforced_full >= 10, "AC 1 requires ≥10 full-coverage controls with evidence, got {enforced_full}" ); assert_eq!(report.summary.not_enforced, 0); assert!( report.summary.gaps >= 1, "gaps must be documented, not hidden" ); } // ── EU AI Act — full-coverage proofs ───────────────────────────────────────── /// The audit-trail proofs share one process-global hash-chain tail and the /// data-dir env var, so they run inside ONE #[test] in a controlled order. /// The mapping-named functions below are called from here; the drift guard /// only requires the named `fn`s to exist. #[test] fn audit_chain_proofs_run_sequentially() { let tmp = tempfile::tempdir().expect("tempdir"); // TODO: Audit that the environment access only happens in single-threaded code. unsafe { std::env::set_var("LEAN_CTX_DATA_DIR", tmp.path()) }; aia_12_1_logging_is_automatic_and_chained(); aia_12_2_actions_attribute_agent_and_role(); // Destroys the chain — must run last. aia_12_1_tampered_log_fails_verification(tmp.path()); // TODO: Audit that the environment access only happens in single-threaded code. unsafe { std::env::remove_var("LEAN_CTX_DATA_DIR") }; } fn aia_12_1_logging_is_automatic_and_chained() { use lean_ctx::core::audit_trail::{self, AuditEntryData, AuditEventType}; audit_trail::record(AuditEntryData { agent_id: "agent-1".into(), tool: "ctx_read".into(), action: Some("full".into()), input_hash: audit_trail::hash_input(&serde_json::Map::new()), output_tokens: 10, role: "developer".into(), event_type: AuditEventType::ToolCall, }); audit_trail::record(AuditEntryData { agent_id: "agent-1".into(), tool: "ctx_search".into(), action: None, input_hash: audit_trail::hash_input(&serde_json::Map::new()), output_tokens: 5, role: "developer".into(), event_type: AuditEventType::ToolCall, }); let entries = audit_trail::load_recent(10); assert!(entries.len() >= 2, "every call recorded without opt-in"); assert!(audit_trail::verify_chain().valid, "hash chain verifies"); } fn aia_12_2_actions_attribute_agent_and_role() { use lean_ctx::core::audit_trail::{self, AuditEntryData, AuditEventType}; audit_trail::record(AuditEntryData { agent_id: "agent-42".into(), tool: "ctx_read".into(), action: None, input_hash: audit_trail::hash_input(&serde_json::Map::new()), output_tokens: 1, role: "reviewer".into(), event_type: AuditEventType::ToolCall, }); let entries = audit_trail::load_recent(1); assert_eq!(entries[0].agent_id, "agent-42"); assert_eq!(entries[0].role, "reviewer"); } fn aia_12_1_tampered_log_fails_verification(data_dir: &std::path::Path) { use lean_ctx::core::audit_trail; assert!(audit_trail::verify_chain().valid); // Tamper with one recorded value — the chain must break, provably. let trail = data_dir.join("audit").join("trail.jsonl"); let content = std::fs::read_to_string(&trail).expect("trail"); let tampered = content.replacen("\"output_tokens\":10", "\"output_tokens\":999", 1); assert_ne!(content, tampered, "tamper fixture must change the log"); std::fs::write(&trail, tampered).expect("write"); let result = audit_trail::verify_chain(); assert!(!result.valid, "tampered log MUST fail verification"); assert!(result.first_invalid_at.is_some()); } #[test] fn aia_12_2a_risk_event_types_are_recorded() { use lean_ctx::core::audit_trail::AuditEventType; // The risk-relevant situations of Art. 12(2)(a) are first-class typed // events — serialization is the registry contract (OCP R3). for (event, wire) in [ (AuditEventType::ToolDenied, "tool_denied"), (AuditEventType::PathJailViolation, "path_jail_violation"), (AuditEventType::BudgetExceeded, "budget_exceeded"), (AuditEventType::SecurityViolation, "security_violation"), (AuditEventType::SecretDetected, "secret_detected"), (AuditEventType::RateLimited, "rate_limited"), (AuditEventType::CrossProjectAccess, "cross_project_access"), ] { assert_eq!( serde_json::to_value(&event).expect("serializes"), serde_json::Value::String(wire.to_string()) ); } } #[test] fn aia_26_6_reference_pack_declares_six_month_retention() { let policy = resolved("eu-ai-act-deployer"); assert!(policy.audit_retention_days.expect("retention declared") >= 180); let (status, _) = row_status("eu-ai-act", "AIA-26.6", "eu-ai-act-deployer"); assert_eq!(status, RowStatus::Enforced); // Violation: baseline declares 90 d — below the Art. 26(6) floor. let (status, detail) = row_status("eu-ai-act", "AIA-26.6", "baseline"); assert_eq!(status, RowStatus::NotEnforced, "{detail}"); } #[test] fn aia_10_5_regulated_identifiers_are_redacted() { let (status, detail) = row_status("eu-ai-act", "AIA-10.5", "eu-ai-act-deployer"); assert_eq!(status, RowStatus::Enforced, "{detail}"); // Violation: a pack without identifier patterns fails the claim. let (status, _) = row_status("eu-ai-act", "AIA-10.5", "baseline"); assert_eq!(status, RowStatus::NotEnforced); } #[test] fn aia_15_5_credentials_never_reach_context() { let (status, detail) = row_status("eu-ai-act", "AIA-15.5-secrets", "eu-ai-act-deployer"); assert_eq!(status, RowStatus::Enforced, "{detail}"); // Direct fixture proof on the resolved patterns. let policy = resolved("eu-ai-act-deployer"); let patterns: Vec = policy .redaction .values() .filter_map(|raw| regex::Regex::new(raw).ok()) .collect(); for fixture in [ "-----BEGIN RSA PRIVATE KEY-----", "AKIAIOSFODNN7EXAMPLE", "api_key = \"sk-supersecretvalue1234\"", "Authorization: Bearer abcdefghij0123456789xyz", ] { assert!( patterns.iter().any(|re| re.is_match(fixture)), "credential fixture must be redacted: {fixture}" ); } } #[test] fn aia_15_5_unauthorized_tool_use_is_blocked() { use lean_ctx::core::capabilities::check_capabilities; // Engine layer: default-deny capability gate blocks the call… let check = check_capabilities("reviewer", "ctx_edit"); assert!(!check.allowed); assert!(!check.missing.is_empty()); // …and the violation surfaces as an exportable governance event. let event = LeanCtxEvent { id: 1, timestamp: chrono::Utc::now().to_rfc3339(), kind: EventKind::PolicyViolation { role: "reviewer".into(), tool: "ctx_edit".into(), reason: "missing capability fs:write".into(), }, }; let exported = ocp::export_event(&event).expect("violation exports"); assert_eq!(exported["kind"]["type"], "policy_violation"); // Pack layer: the reference pack scopes the tool surface. let (status, _) = row_status("eu-ai-act", "AIA-15.5-access", "eu-ai-act-deployer"); assert_eq!(status, RowStatus::Enforced); } #[test] fn aia_14_4a_capabilities_are_inspectable() { let grant = ocp::capability_grant_set("developer"); let caps = grant["capabilities"].as_array().expect("array"); assert!( !caps.is_empty(), "oversight needs a non-empty capability view" ); assert_eq!(grant["subject"], "developer"); } #[test] fn aia_14_4e_budget_caps_bound_operation() { let policy = resolved("eu-ai-act-deployer"); assert!(policy.max_context_tokens.expect("cap declared") > 0); // Intervention signals exist before and at exhaustion. for kind in [ EventKind::BudgetWarning { role: "developer".into(), dimension: "tokens".into(), used: "9000".into(), limit: "12000".into(), percent: 75, }, EventKind::BudgetExhausted { role: "developer".into(), dimension: "tokens".into(), used: "12001".into(), limit: "12000".into(), }, ] { let event = LeanCtxEvent { id: 1, timestamp: chrono::Utc::now().to_rfc3339(), kind, }; assert!(ocp::export_event(&event).is_some()); } // Violation: a pack without a cap does not earn the claim. let (status, _) = row_status("eu-ai-act", "AIA-14.4e", "open-source"); assert_eq!(status, RowStatus::NotEnforced); } #[test] fn aia_13_1_context_ir_attributes_every_item() { use lean_ctx::core::context_ir::{ContextIrSourceKindV1, ContextIrV1, RecordIrInput}; let mut ir = ContextIrV1::new(); ir.record(RecordIrInput { kind: ContextIrSourceKindV1::Read, tool: "ctx_read", client_name: Some("cursor".into()), agent_id: Some("agent-1".into()), path: Some("src/main.rs"), command: None, pattern: None, input_tokens: 100, output_tokens: 10, duration: std::time::Duration::from_millis(1), content_excerpt: "fn main() {}", }); let item = &ir.items[0]; assert_eq!(item.source.tool, "ctx_read"); assert!( item.verification.content_md5.is_some(), "verifiable excerpt" ); assert!(item.safety.redacted, "redaction ran before storage"); } // ── ISO 42001 — full-coverage proofs ───────────────────────────────────────── #[test] fn iso_a626_operation_logging_always_on() { // Same engine guarantee as AIA-12.1; the mapping row must say so. let mapping = compliance::get("iso42001").expect("mapping"); let report = compliance::report(mapping, None); let row = report .rows .iter() .find(|r| r.id == "ISO-A.6.2.6") .expect("row"); assert_eq!(row.status, RowStatus::EngineGuarantee); } #[test] fn iso_a74_context_data_is_filtered_before_use() { let (status, detail) = row_status("iso42001", "ISO-A.7.4", "iso42001-aligned"); assert_eq!(status, RowStatus::Enforced, "{detail}"); } #[test] fn iso_a82_resolved_policy_is_exportable() { let policy = resolved("iso42001-aligned"); let json = serde_json::to_value(&policy).expect("resolved policy serializes"); assert!(json["name"].is_string()); assert!(json["redaction"].is_object()); } #[test] fn iso_a92_policy_pack_defines_enforced_process() { let (status, detail) = row_status("iso42001", "ISO-A.9.2", "iso42001-aligned"); assert_eq!(status, RowStatus::Enforced, "{detail}"); // Violation: an empty-process pack cannot claim A.9.2. let (status, _) = row_status("iso42001", "ISO-A.9.2", "open-source"); assert_eq!(status, RowStatus::NotEnforced); } #[test] fn iso_a94_out_of_scope_use_is_blocked_and_recorded() { let policy = resolved("iso42001-aligned"); assert!(policy.deny_tools.iter().any(|t| t == "ctx_url_read")); use lean_ctx::core::audit_trail::AuditEventType; assert_eq!( serde_json::to_value(AuditEventType::ToolDenied).expect("serializes"), serde_json::Value::String("tool_denied".into()) ); let (status, _) = row_status("iso42001", "ISO-A.9.4", "iso42001-aligned"); assert_eq!(status, RowStatus::Enforced); } // ── SOC 2 — full-coverage proofs ───────────────────────────────────────────── #[test] fn soc2_cc61_default_deny_capability_gate() { use lean_ctx::core::capabilities::check_capabilities; // minimal role: read-only — everything mutating is denied by default. for tool in ["ctx_edit", "ctx_shell", "ctx_agent"] { let check = check_capabilities("minimal", tool); assert!(!check.allowed, "{tool} must be denied for minimal role"); } let (status, _) = row_status("soc2", "SOC2-CC6.1", "soc2-context"); assert_eq!(status, RowStatus::Enforced); } #[test] fn soc2_cc66_egress_deniable_and_path_jailed() { let (status, detail) = row_status("soc2", "SOC2-CC6.6", "soc2-context"); assert_eq!(status, RowStatus::Enforced, "{detail}"); use lean_ctx::core::audit_trail::AuditEventType; assert_eq!( serde_json::to_value(AuditEventType::PathJailViolation).expect("serializes"), serde_json::Value::String("path_jail_violation".into()) ); // Violation: baseline denies nothing. let (status, _) = row_status("soc2", "SOC2-CC6.6", "baseline"); assert_eq!(status, RowStatus::NotEnforced); } #[test] fn soc2_cc72_security_events_are_typed_and_recorded() { use lean_ctx::core::audit_trail::AuditEventType; for event in [ AuditEventType::SecurityViolation, AuditEventType::SecretDetected, AuditEventType::RateLimited, AuditEventType::BudgetExceeded, ] { let wire = serde_json::to_value(&event).expect("serializes"); assert!(wire.is_string(), "typed security event: {wire}"); } } #[test] fn soc2_cc71_role_changes_are_audited() { use lean_ctx::core::audit_trail::AuditEventType; assert_eq!( serde_json::to_value(AuditEventType::RoleChanged).expect("serializes"), serde_json::Value::String("role_changed".into()) ); let event = LeanCtxEvent { id: 1, timestamp: chrono::Utc::now().to_rfc3339(), kind: EventKind::RoleChanged { from: "developer".into(), to: "reviewer".into(), }, }; assert!(ocp::export_event(&event).is_some()); } #[test] fn soc2_c11_confidential_material_redacted() { let (status, detail) = row_status("soc2", "SOC2-C1.1", "soc2-context"); assert_eq!(status, RowStatus::Enforced, "{detail}"); // Violation: a pack with no redaction at all cannot claim C1.1. (Every // builtin inherits the baseline credential patterns — by design — so // the violation fixture is a synthetic root pack.) let bare = lean_ctx::core::policy::parse( "name = \"bare\"\nversion = \"1.0.0\"\ndescription = \"no redaction\"\n", ) .expect("parses"); let resolved_bare = resolve(&bare).expect("resolves"); let mapping = compliance::get("soc2").expect("mapping"); let report = compliance::report(mapping, Some(&resolved_bare)); let row = report .rows .iter() .find(|r| r.id == "SOC2-C1.1") .expect("row"); assert_eq!(row.status, RowStatus::NotEnforced); }