diff --git a/compliance-agent/src/controls/surface.rs b/compliance-agent/src/controls/surface.rs index ade5bc1..0e37932 100644 --- a/compliance-agent/src/controls/surface.rs +++ b/compliance-agent/src/controls/surface.rs @@ -47,6 +47,17 @@ pub const SURFACES: &[Surface] = &[ "ratelimit", ], }, + Surface { + control_id: "cra-ai-12", // Rollenbasierte Autorisierung (RBAC) + terms: &[ + "authorize", + "permission", + "role", + "rbac", + "require_role", + "has_role", + ], + }, Surface { control_id: "cra-ai-24", // Security-Logging terms: &["login", "authorize", "permission", "role", "admin", "audit"], @@ -196,10 +207,11 @@ mod tests { #[test] fn surfaces_cover_the_absence_based_controls() { - assert_eq!(SURFACES.len(), 7); + assert_eq!(SURFACES.len(), 8); for id in [ "cra-ai-6", "cra-ai-11", + "cra-ai-12", "cra-ai-24", "cra-ai-27", "cra-ai-28", diff --git a/control-map/data/cra_control_map.json b/control-map/data/cra_control_map.json index 9705e63..f445025 100644 --- a/control-map/data/cra_control_map.json +++ b/control-map/data/cra_control_map.json @@ -25,29 +25,29 @@ "control": "cra-ai-2", "title": "Minimale Angriffsflaeche", "scans": [], - "note": "code-checkable but no off-the-shelf tool digs it out — author a detector (custom semgrep rule / check)", - "status": "needs_tooling" + "note": "design property (minimal attack surface) — not derivable from local code patterns; architecture/threat-model review", + "status": "not_code_checkable" }, { "control": "cra-ai-3", "title": "Sichere Systemarchitektur", "scans": [], - "note": "code-checkable but no off-the-shelf tool digs it out — author a detector (custom semgrep rule / check)", - "status": "needs_tooling" + "note": "design property (secure system architecture) — architecture review, not statically code-checkable", + "status": "not_code_checkable" }, { "control": "cra-ai-4", "title": "Least-Privilege-Prinzip", "scans": [], - "note": "code-checkable but no off-the-shelf tool digs it out — author a detector (custom semgrep rule / check)", - "status": "needs_tooling" + "note": "design property (least-privilege) — deployment/IAM & architecture review, not a local code pattern", + "status": "not_code_checkable" }, { "control": "cra-ai-5", "title": "Manipulationsschutz", "scans": [], - "note": "code-checkable but no off-the-shelf tool digs it out — author a detector (custom semgrep rule / check)", - "status": "needs_tooling" + "note": "design property (tamper protection) — hardware/runtime & operational control, not statically code-checkable", + "status": "not_code_checkable" }, { "control": "cra-ai-6", @@ -146,7 +146,7 @@ "control": "cra-ai-12", "title": "Rollenbasierte Autorisierung", "scans": [], - "note": "code-checkable but no off-the-shelf tool digs it out — author a detector (custom semgrep rule / check)", + "note": "absence-based — no syntactic pattern; covered by the grounded surface check (retrieve surface + LLM judge), gated (BREAKPILOT_GROUNDED_CHECKS) pending live tuning", "status": "needs_tooling" }, { diff --git a/control-map/src/lib.rs b/control-map/src/lib.rs index 8db0ede..63107a8 100644 --- a/control-map/src/lib.rs +++ b/control-map/src/lib.rs @@ -214,6 +214,27 @@ mod tests { assert!(hits.iter().any(|c| c.control == "cra-ai-1")); } + #[test] + fn coverage_reflects_the_b_track_split() { + let s = ControlMap::cra().unwrap().summary(); + // 9 already tool-covered + B1's 4 custom-semgrep controls. + assert_eq!(s.covered, 13); + // The 8 grounded surface controls stay needs_tooling until live-tuned. + assert_eq!(s.needs_tooling, 8); + // B3 marked the 4 pure-architectural controls not code-checkable. + assert_eq!(s.not_code_checkable, 19); + } + + #[test] + fn architectural_controls_are_not_code_checkable() { + let map = ControlMap::cra().unwrap(); + for id in ["cra-ai-2", "cra-ai-3", "cra-ai-4", "cra-ai-5"] { + let c = map.coverage(id).unwrap(); + assert_eq!(c.status, Coverage::NotCodeCheckable, "{id}"); + assert!(c.scans.is_empty(), "{id} should carry no scan bindings"); + } + } + #[test] fn custom_rule_controls_do_not_bind_by_broad_cwe() { let map = ControlMap::cra().unwrap();