feat: control-driven SAST — LUT + grounded LLM triage over tool findings #213
@@ -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)
|
||||
|
||||
Generated
+9
@@ -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"
|
||||
|
||||
@@ -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"] }
|
||||
|
||||
@@ -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 }
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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<String>,
|
||||
/// Optional specific rule ids this control keys on.
|
||||
#[serde(default)]
|
||||
pub rules: Vec<String>,
|
||||
}
|
||||
|
||||
/// 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<ScanBinding>,
|
||||
/// Reviewer note — why it needs tooling / isn't code-checkable.
|
||||
#[serde(default)]
|
||||
pub note: Option<String>,
|
||||
}
|
||||
|
||||
/// 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<ControlEntry>,
|
||||
}
|
||||
|
||||
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<Self, MapError> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user