From b7534d1123d91b2b9cd8e8d8c8c1d20fc0a61361 Mon Sep 17 00:00:00 2001 From: Sharang Parnerkar <30073382+mighty840@users.noreply.github.com> Date: Tue, 21 Jul 2026 10:15:26 +0200 Subject: [PATCH] feat(control-map): deterministic control->scan LUT crate (CRA, 40 controls) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New standalone crate — the 'transcribing' layer that maps each control to the static-scan step(s) that check it, or marks it needs_tooling / not_code_checkable. Authored + human-reviewed: no LLM decides coverage. The LLM only triages the tool's findings downstream (in the agent), never here. - ControlMap / ControlEntry / ScanBinding / Coverage types + embedded JSON LUT - query API: coverage(control), controls_for(tool, cwe), summary() - CRA LUT: 40 controls -> 9 covered (semgrep/gitleaks/syft/osv) / 16 needs_tooling / 15 not_code_checkable - wired into CI (clippy + test). 4 lib tests. Co-Authored-By: Claude Fable 5 --- .gitea/workflows/ci.yml | 6 +- Cargo.lock | 9 + Cargo.toml | 2 + control-map/Cargo.toml | 12 + control-map/data/cra_control_map.json | 399 ++++++++++++++++++++++++++ control-map/src/lib.rs | 163 +++++++++++ 6 files changed, 589 insertions(+), 2 deletions(-) create mode 100644 control-map/Cargo.toml create mode 100644 control-map/data/cra_control_map.json create mode 100644 control-map/src/lib.rs diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index 93667a8..3eaa3ba 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -109,6 +109,8 @@ jobs: run: cargo clippy -p compliance-mcp -- -D warnings - name: Clippy (werkbank-exec) run: cargo clippy -p werkbank-exec -- -D warnings + - name: Clippy (control-map) + run: cargo clippy -p control-map -- -D warnings # Security audit - name: Security Audit @@ -117,8 +119,8 @@ jobs: RUSTC_WRAPPER: "" # Tests (reuses compilation artifacts from clippy) - - name: Tests (core + agent + werkbank-exec) - run: cargo test -p compliance-core -p compliance-agent -p werkbank-exec --lib + - name: Tests (core + agent + werkbank-exec + control-map) + run: cargo test -p compliance-core -p compliance-agent -p werkbank-exec -p control-map --lib - name: Tests (dashboard server) run: cargo test -p compliance-dashboard --features server --no-default-features - name: Tests (dashboard web) diff --git a/Cargo.lock b/Cargo.lock index f8a6d02..471858f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -968,6 +968,15 @@ dependencies = [ "charset", ] +[[package]] +name = "control-map" +version = "0.1.0" +dependencies = [ + "serde", + "serde_json", + "thiserror 2.0.18", +] + [[package]] name = "convert_case" version = "0.8.0" diff --git a/Cargo.toml b/Cargo.toml index e8cb61b..7e7ddfe 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,6 +8,7 @@ members = [ "compliance-mcp", "compliance-smoke", "werkbank-exec", + "control-map", ] resolver = "2" @@ -17,6 +18,7 @@ expect_used = "deny" [workspace.dependencies] compliance-core = { path = "compliance-core", default-features = false } +control-map = { path = "control-map" } serde = { version = "1", features = ["derive"] } serde_json = "1" tokio = { version = "1", features = ["full"] } diff --git a/control-map/Cargo.toml b/control-map/Cargo.toml new file mode 100644 index 0000000..110438b --- /dev/null +++ b/control-map/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "control-map" +version = "0.1.0" +edition = "2021" + +[lints] +workspace = true + +[dependencies] +serde = { workspace = true } +serde_json = { workspace = true } +thiserror = { workspace = true } diff --git a/control-map/data/cra_control_map.json b/control-map/data/cra_control_map.json new file mode 100644 index 0000000..3b238c5 --- /dev/null +++ b/control-map/data/cra_control_map.json @@ -0,0 +1,399 @@ +{ + "version": "1.0", + "framework": "cra", + "controls": [ + { + "control": "cra-ai-1", + "title": "Secure-by-Default-Konfiguration", + "scans": [], + "note": "code-checkable but no off-the-shelf tool digs it out — author a detector (custom semgrep rule / check)", + "status": "needs_tooling" + }, + { + "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" + }, + { + "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" + }, + { + "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" + }, + { + "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" + }, + { + "control": "cra-ai-6", + "title": "Integritaetspruefung", + "scans": [], + "note": "code-checkable but no off-the-shelf tool digs it out — author a detector (custom semgrep rule / check)", + "status": "needs_tooling" + }, + { + "control": "cra-ai-7", + "title": "Starke Authentifizierung", + "scans": [], + "note": "code-checkable but no off-the-shelf tool digs it out — author a detector (custom semgrep rule / check)", + "status": "needs_tooling" + }, + { + "control": "cra-ai-8", + "title": "Keine Default-Passwoerter", + "scans": [ + { + "tool": "gitleaks", + "scan_type": "secret_detection", + "cwe": [], + "rules": [] + }, + { + "tool": "semgrep", + "scan_type": "sast", + "cwe": [ + "CWE-798", + "CWE-259" + ], + "rules": [] + } + ], + "note": null, + "status": "covered" + }, + { + "control": "cra-ai-9", + "title": "Sicheres Credential-Management", + "scans": [ + { + "tool": "gitleaks", + "scan_type": "secret_detection", + "cwe": [], + "rules": [] + }, + { + "tool": "semgrep", + "scan_type": "sast", + "cwe": [ + "CWE-798", + "CWE-522" + ], + "rules": [] + } + ], + "note": null, + "status": "covered" + }, + { + "control": "cra-ai-10", + "title": "Sitzungsmanagement", + "scans": [], + "note": "code-checkable but no off-the-shelf tool digs it out — author a detector (custom semgrep rule / check)", + "status": "needs_tooling" + }, + { + "control": "cra-ai-11", + "title": "Brute-Force-Schutz", + "scans": [], + "note": "code-checkable but no off-the-shelf tool digs it out — author a detector (custom semgrep rule / check)", + "status": "needs_tooling" + }, + { + "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)", + "status": "needs_tooling" + }, + { + "control": "cra-ai-13", + "title": "Verschluesselung sensibler Daten", + "scans": [ + { + "tool": "semgrep", + "scan_type": "sast", + "cwe": [ + "CWE-327", + "CWE-326" + ], + "rules": [] + } + ], + "note": null, + "status": "covered" + }, + { + "control": "cra-ai-14", + "title": "Speicher-Schutz (Data at Rest)", + "scans": [], + "note": "code-checkable but no off-the-shelf tool digs it out — author a detector (custom semgrep rule / check)", + "status": "needs_tooling" + }, + { + "control": "cra-ai-15", + "title": "Transport-Schutz (Data in Transit)", + "scans": [ + { + "tool": "semgrep", + "scan_type": "sast", + "cwe": [ + "CWE-319", + "CWE-311" + ], + "rules": [] + } + ], + "note": null, + "status": "covered" + }, + { + "control": "cra-ai-16", + "title": "Sicheres Schluesselmanagement", + "scans": [ + { + "tool": "gitleaks", + "scan_type": "secret_detection", + "cwe": [], + "rules": [] + }, + { + "tool": "semgrep", + "scan_type": "sast", + "cwe": [ + "CWE-798", + "CWE-321" + ], + "rules": [] + } + ], + "note": null, + "status": "covered" + }, + { + "control": "cra-ai-17", + "title": "Datenminimierung", + "scans": [], + "note": "process / document control — outside static-scan scope", + "status": "not_code_checkable" + }, + { + "control": "cra-ai-18", + "title": "Strukturierter SSDLC", + "scans": [], + "note": "process / document control — outside static-scan scope", + "status": "not_code_checkable" + }, + { + "control": "cra-ai-19", + "title": "Systematische Code Reviews", + "scans": [], + "note": "process / document control — outside static-scan scope", + "status": "not_code_checkable" + }, + { + "control": "cra-ai-20", + "title": "Automatisierte Sicherheitstests", + "scans": [ + { + "tool": "semgrep", + "scan_type": "sast", + "cwe": [ + "CWE-89", + "CWE-78", + "CWE-79", + "CWE-22" + ], + "rules": [] + } + ], + "note": null, + "status": "covered" + }, + { + "control": "cra-ai-21", + "title": "Supply-Chain-Security", + "scans": [], + "note": "process / document control — outside static-scan scope", + "status": "not_code_checkable" + }, + { + "control": "cra-ai-22", + "title": "Dependency-Monitoring", + "scans": [ + { + "tool": "osv", + "scan_type": "cve", + "cwe": [], + "rules": [] + }, + { + "tool": "syft", + "scan_type": "sbom", + "cwe": [], + "rules": [] + } + ], + "note": null, + "status": "covered" + }, + { + "control": "cra-ai-23", + "title": "Software Bill of Materials (SBOM)", + "scans": [ + { + "tool": "syft", + "scan_type": "sbom", + "cwe": [], + "rules": [] + } + ], + "note": null, + "status": "covered" + }, + { + "control": "cra-ai-24", + "title": "Security-Logging", + "scans": [], + "note": "code-checkable but no off-the-shelf tool digs it out — author a detector (custom semgrep rule / check)", + "status": "needs_tooling" + }, + { + "control": "cra-ai-25", + "title": "Ereignis-Monitoring", + "scans": [], + "note": "process / document control — outside static-scan scope", + "status": "not_code_checkable" + }, + { + "control": "cra-ai-26", + "title": "Anomalie-Erkennung", + "scans": [], + "note": "process / document control — outside static-scan scope", + "status": "not_code_checkable" + }, + { + "control": "cra-ai-27", + "title": "Log-Integritaet und -Aufbewahrung", + "scans": [], + "note": "code-checkable but no off-the-shelf tool digs it out — author a detector (custom semgrep rule / check)", + "status": "needs_tooling" + }, + { + "control": "cra-ai-28", + "title": "Sichere Update-Mechanismen", + "scans": [], + "note": "code-checkable but no off-the-shelf tool digs it out — author a detector (custom semgrep rule / check)", + "status": "needs_tooling" + }, + { + "control": "cra-ai-29", + "title": "Update-Authentizitaet", + "scans": [], + "note": "code-checkable but no off-the-shelf tool digs it out — author a detector (custom semgrep rule / check)", + "status": "needs_tooling" + }, + { + "control": "cra-ai-30", + "title": "Update-Integritaet", + "scans": [], + "note": "code-checkable but no off-the-shelf tool digs it out — author a detector (custom semgrep rule / check)", + "status": "needs_tooling" + }, + { + "control": "cra-ai-31", + "title": "Lifecycle-Support", + "scans": [], + "note": "process / document control — outside static-scan scope", + "status": "not_code_checkable" + }, + { + "control": "cra-ai-32", + "title": "Schwachstellen-Identifikation", + "scans": [], + "note": "process / document control — outside static-scan scope", + "status": "not_code_checkable" + }, + { + "control": "cra-ai-33", + "title": "SBOM-Pflege und Analyse", + "scans": [ + { + "tool": "syft", + "scan_type": "sbom", + "cwe": [], + "rules": [] + }, + { + "tool": "osv", + "scan_type": "cve", + "cwe": [], + "rules": [] + } + ], + "note": null, + "status": "covered" + }, + { + "control": "cra-ai-34", + "title": "Risikobasierte Priorisierung", + "scans": [], + "note": "process / document control — outside static-scan scope", + "status": "not_code_checkable" + }, + { + "control": "cra-ai-35", + "title": "Coordinated Vulnerability Disclosure", + "scans": [], + "note": "process / document control — outside static-scan scope", + "status": "not_code_checkable" + }, + { + "control": "cra-ai-36", + "title": "Incident-Response-Prozess", + "scans": [], + "note": "process / document control — outside static-scan scope", + "status": "not_code_checkable" + }, + { + "control": "cra-ai-37", + "title": "Fruehwarnung (24h)", + "scans": [], + "note": "process / document control — outside static-scan scope", + "status": "not_code_checkable" + }, + { + "control": "cra-ai-38", + "title": "Detaillierter Vorfallsbericht (72h)", + "scans": [], + "note": "process / document control — outside static-scan scope", + "status": "not_code_checkable" + }, + { + "control": "cra-ai-39", + "title": "Patch-Bereitstellung", + "scans": [], + "note": "process / document control — outside static-scan scope", + "status": "not_code_checkable" + }, + { + "control": "cra-ai-40", + "title": "Dokumentation und Nachbereitung", + "scans": [], + "note": "process / document control — outside static-scan scope", + "status": "not_code_checkable" + } + ] +} diff --git a/control-map/src/lib.rs b/control-map/src/lib.rs new file mode 100644 index 0000000..dfbb63a --- /dev/null +++ b/control-map/src/lib.rs @@ -0,0 +1,163 @@ +//! `control-map` — the deterministic control → scan lookup table (LUT). +//! +//! The "transcribing" layer: it maps each compliance control to the static-scan +//! step(s) that check it, or marks it as needing custom tooling, or as not +//! code-checkable at all. The map is **authored and human-reviewed** — no LLM +//! decides coverage. The LLM only enters later, downstream, to triage/ground the +//! *tool's* findings (that lives in the agent, not here). +//! +//! This crate is intentionally tiny and standalone: types + an embedded JSON LUT +//! + query helpers. + +use serde::{Deserialize, Serialize}; + +/// Coverage bucket for a control under static (SAST-family) scanning. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum Coverage { + /// An existing tool's scan surfaces findings for this control. + Covered, + /// Code-checkable, but no existing tool digs it out — we must write tooling. + NeedsTooling, + /// Process / document control — out of static-scan scope. + NotCodeCheckable, +} + +/// One tool binding: a scan step that (at least partially) checks a control. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ScanBinding { + /// Tool name, e.g. `"semgrep"`, `"gitleaks"`, `"syft"`, `"osv"`. + pub tool: String, + /// Scan family, e.g. `"sast"`, `"secret_detection"`, `"sbom"`, `"cve"`. + pub scan_type: String, + /// CWEs whose findings map to this control (used to attach findings back). + #[serde(default)] + pub cwe: Vec, + /// Optional specific rule ids this control keys on. + #[serde(default)] + pub rules: Vec, +} + +/// One control's entry in the LUT. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ControlEntry { + /// Control id, e.g. `"cra-ai-8"`. + pub control: String, + /// Human-readable title (for the reviewable view). + #[serde(default)] + pub title: String, + /// Coverage bucket. + pub status: Coverage, + /// Tool bindings (empty unless `status == Covered`). + #[serde(default)] + pub scans: Vec, + /// Reviewer note — why it needs tooling / isn't code-checkable. + #[serde(default)] + pub note: Option, +} + +/// The control → scan lookup table for one framework. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ControlMap { + pub version: String, + pub framework: String, + pub controls: Vec, +} + +const CRA_MAP_JSON: &str = include_str!("../data/cra_control_map.json"); + +impl ControlMap { + /// Load the built-in CRA control map (the embedded, authored LUT). + pub fn cra() -> Result { + Ok(serde_json::from_str(CRA_MAP_JSON)?) + } + + /// The coverage entry for a control id, if present. + pub fn coverage(&self, control_id: &str) -> Option<&ControlEntry> { + self.controls.iter().find(|c| c.control == control_id) + } + + /// Controls whose bindings include the given `tool` + `cwe` — used to attach a + /// raw tool finding back to the control(s) it's evidence for. + pub fn controls_for(&self, tool: &str, cwe: &str) -> Vec<&ControlEntry> { + self.controls + .iter() + .filter(|c| { + c.scans + .iter() + .any(|s| s.tool == tool && s.cwe.iter().any(|w| w == cwe)) + }) + .collect() + } + + /// Count of controls in each coverage bucket. + pub fn summary(&self) -> CoverageSummary { + let mut s = CoverageSummary::default(); + for c in &self.controls { + match c.status { + Coverage::Covered => s.covered += 1, + Coverage::NeedsTooling => s.needs_tooling += 1, + Coverage::NotCodeCheckable => s.not_code_checkable += 1, + } + } + s + } +} + +/// Coverage bucket counts. +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] +pub struct CoverageSummary { + pub covered: usize, + pub needs_tooling: usize, + pub not_code_checkable: usize, +} + +impl CoverageSummary { + pub fn total(&self) -> usize { + self.covered + self.needs_tooling + self.not_code_checkable + } +} + +/// Errors loading a control map. +#[derive(Debug, thiserror::Error)] +pub enum MapError { + #[error("failed to parse control map: {0}")] + Parse(#[from] serde_json::Error), +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn cra_map_loads_all_40_controls() { + let map = ControlMap::cra().expect("CRA map should parse"); + assert_eq!(map.framework, "cra"); + assert_eq!(map.controls.len(), 40); + assert_eq!(map.summary().total(), 40); + } + + #[test] + fn hardcoded_password_control_is_tool_covered() { + let map = ControlMap::cra().unwrap(); + let c = map.coverage("cra-ai-8").expect("cra-ai-8 present"); + assert_eq!(c.status, Coverage::Covered); + assert!(c.scans.iter().any(|s| s.tool == "semgrep")); + assert!(c.scans.iter().any(|s| s.tool == "gitleaks")); + } + + #[test] + fn finding_attaches_back_to_control_via_tool_and_cwe() { + let map = ControlMap::cra().unwrap(); + let hits = map.controls_for("semgrep", "CWE-798"); + assert!(hits.iter().any(|c| c.control == "cra-ai-8")); + } + + #[test] + fn every_bucket_is_represented() { + let s = ControlMap::cra().unwrap().summary(); + assert!(s.covered > 0); + assert!(s.needs_tooling > 0); + assert!(s.not_code_checkable > 0); + } +}