feat(control-map): deterministic control->scan LUT crate (CRA, 40 controls)
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 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
d1f42a1a83
commit
b7534d1123
@@ -109,6 +109,8 @@ jobs:
|
|||||||
run: cargo clippy -p compliance-mcp -- -D warnings
|
run: cargo clippy -p compliance-mcp -- -D warnings
|
||||||
- name: Clippy (werkbank-exec)
|
- name: Clippy (werkbank-exec)
|
||||||
run: cargo clippy -p werkbank-exec -- -D warnings
|
run: cargo clippy -p werkbank-exec -- -D warnings
|
||||||
|
- name: Clippy (control-map)
|
||||||
|
run: cargo clippy -p control-map -- -D warnings
|
||||||
|
|
||||||
# Security audit
|
# Security audit
|
||||||
- name: Security Audit
|
- name: Security Audit
|
||||||
@@ -117,8 +119,8 @@ jobs:
|
|||||||
RUSTC_WRAPPER: ""
|
RUSTC_WRAPPER: ""
|
||||||
|
|
||||||
# Tests (reuses compilation artifacts from clippy)
|
# Tests (reuses compilation artifacts from clippy)
|
||||||
- name: Tests (core + agent + werkbank-exec)
|
- name: Tests (core + agent + werkbank-exec + control-map)
|
||||||
run: cargo test -p compliance-core -p compliance-agent -p werkbank-exec --lib
|
run: cargo test -p compliance-core -p compliance-agent -p werkbank-exec -p control-map --lib
|
||||||
- name: Tests (dashboard server)
|
- name: Tests (dashboard server)
|
||||||
run: cargo test -p compliance-dashboard --features server --no-default-features
|
run: cargo test -p compliance-dashboard --features server --no-default-features
|
||||||
- name: Tests (dashboard web)
|
- name: Tests (dashboard web)
|
||||||
|
|||||||
Generated
+9
@@ -968,6 +968,15 @@ dependencies = [
|
|||||||
"charset",
|
"charset",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "control-map"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"thiserror 2.0.18",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "convert_case"
|
name = "convert_case"
|
||||||
version = "0.8.0"
|
version = "0.8.0"
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ members = [
|
|||||||
"compliance-mcp",
|
"compliance-mcp",
|
||||||
"compliance-smoke",
|
"compliance-smoke",
|
||||||
"werkbank-exec",
|
"werkbank-exec",
|
||||||
|
"control-map",
|
||||||
]
|
]
|
||||||
resolver = "2"
|
resolver = "2"
|
||||||
|
|
||||||
@@ -17,6 +18,7 @@ expect_used = "deny"
|
|||||||
|
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
compliance-core = { path = "compliance-core", default-features = false }
|
compliance-core = { path = "compliance-core", default-features = false }
|
||||||
|
control-map = { path = "control-map" }
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
tokio = { version = "1", features = ["full"] }
|
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