Files
yvgude--lean-ctx/rust/tests/compliance_frameworks.rs
T
wehub-resource-sync 26382a7ac6
CI / Clippy (push) Failing after 15m13s
CI / Test (ubuntu-latest) (push) Failing after 16m1s
CI / Test (macos-latest) (push) Has been cancelled
CI / Test (windows-latest) (push) Has been cancelled
CI / Build (no embeddings / no ORT) (push) Has been cancelled
CI / Format (push) Has been cancelled
CI / Cookbook (Node) (push) Has been cancelled
CI / Pi Extension (Node) (push) Has been cancelled
CI / Rust SDK (lean-ctx-client) (push) Has been cancelled
CI / Embed SDK (lean-ctx-sdk) (push) Has been cancelled
CI / Python SDK (leanctx) (push) Has been cancelled
CI / Hermes Plugin (Python) (push) Has been cancelled
CI / SDK Conformance Matrix (push) Has been cancelled
CI / Coverage (push) Has been cancelled
CI / cargo-deny (push) Has been cancelled
CI / Adversarial Safety (push) Has been cancelled
CI / Benchmarks (push) Has been cancelled
CI / Output-Quality Gate (eval A/B) (push) Has been cancelled
CI / Documentation (push) Has been cancelled
CI / CI Green (push) Has been cancelled
JetBrains Plugin / Actionlint (push) Has been cancelled
CodeQL / Analyze (actions) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
CodeQL / Analyze (rust) (push) Has been cancelled
JetBrains Plugin / Validation (push) Has been cancelled
JetBrains Plugin / Build (push) Has been cancelled
JetBrains Plugin / Test (push) Has been cancelled
Security Check / Security Scan (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:35:30 +08:00

461 lines
17 KiB
Rust

//! 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<regex::Regex> = 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);
}