feat(cra): NIST/OWASP security golden-set crosswalk + full measure texts in CRA tab
Crosswalk (cra_security_crosswalk.py): deterministic, hand-curated CRA Annex I -> NIST 800-53 Rev5 + OWASP Top 10:2021 mapping, the authoritative Security Golden Set (no RAG; semantic breadth comes later via the shared Controls-API). Mapper attaches NIST/OWASP refs per finding; golden-set completeness pinned by test (every requirement has >=1 NIST ref). CRA tab now shows the NIST/OWASP best- practice refs per finding and the full curated measure texts + norm references (from measures_library_cra.go). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -14,6 +14,7 @@ from dataclasses import dataclass, field, asdict
|
||||
from typing import Optional
|
||||
|
||||
from compliance.api.cra_annex_i_data import ANNEX_I_REQUIREMENTS, MEASURES, DEADLINES
|
||||
from compliance.services.cra_security_crosswalk import security_refs_for
|
||||
|
||||
_REQ_INDEX = {r["req_id"]: r for r in ANNEX_I_REQUIREMENTS}
|
||||
_SEV_ORDER = {"LOW": 1, "MEDIUM": 2, "HIGH": 3, "CRITICAL": 4}
|
||||
@@ -89,6 +90,8 @@ class MappedFinding:
|
||||
iso27001_ref: list = field(default_factory=list)
|
||||
risk_level: str = "LOW"
|
||||
measures: list = field(default_factory=list)
|
||||
nist_refs: list = field(default_factory=list) # NIST 800-53 control IDs (golden-set crosswalk)
|
||||
owasp_refs: list = field(default_factory=list) # [{code, label}] OWASP Top 10:2021
|
||||
rationale: str = ""
|
||||
unmapped: bool = False
|
||||
|
||||
@@ -162,6 +165,7 @@ def map_finding(f: ScannerFinding) -> MappedFinding:
|
||||
for m in _REQ_INDEX[rid].get("mapped_measures", []):
|
||||
if m not in measures:
|
||||
measures.append(m)
|
||||
refs = security_refs_for(reqs)
|
||||
return MappedFinding(
|
||||
finding_id=f.id,
|
||||
requirement_ids=reqs,
|
||||
@@ -170,6 +174,8 @@ def map_finding(f: ScannerFinding) -> MappedFinding:
|
||||
iso27001_ref=list(primary.get("iso27001_ref", [])),
|
||||
risk_level=_SEV_BY_RANK.get(risk_rank, "LOW"),
|
||||
measures=measures,
|
||||
nist_refs=refs["nist"],
|
||||
owasp_refs=refs["owasp"],
|
||||
rationale="{}: {}".format(primary["req_id"], primary.get("title", "")),
|
||||
)
|
||||
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
"""CRA Annex I -> NIST 800-53 Rev5 + OWASP Top 10:2021 crosswalk (Security Golden Set).
|
||||
|
||||
Deterministic, hand-curated mapping. It is the AUTHORITATIVE reference set that
|
||||
the standalone CRA assessment uses to attach best-practice control IDs to each
|
||||
CRA essential requirement. NOT a RAG search — semantic breadth comes later via
|
||||
the shared Controls-API of the mapping session, NOT a second retrieval here.
|
||||
|
||||
Each entry maps a CRA-AI requirement to the well-established NIST 800-53 Rev5
|
||||
control identifiers and the OWASP Top 10:2021 category that govern the same
|
||||
control objective. Process-only requirements (disclosure/reporting) carry NIST
|
||||
refs where defensible and no OWASP code (OWASP Top 10 addresses code-level
|
||||
weaknesses, not disclosure processes) — left empty rather than invented.
|
||||
"""
|
||||
|
||||
# OWASP Top 10:2021 category labels (for display).
|
||||
OWASP_2021 = {
|
||||
"A01:2021": "Broken Access Control",
|
||||
"A02:2021": "Cryptographic Failures",
|
||||
"A04:2021": "Insecure Design",
|
||||
"A05:2021": "Security Misconfiguration",
|
||||
"A06:2021": "Vulnerable and Outdated Components",
|
||||
"A07:2021": "Identification and Authentication Failures",
|
||||
"A08:2021": "Software and Data Integrity Failures",
|
||||
"A09:2021": "Security Logging and Monitoring Failures",
|
||||
}
|
||||
|
||||
# req_id -> {"nist": [800-53 control IDs], "owasp": [Top 10:2021 codes]}
|
||||
CRA_SECURITY_CROSSWALK = {
|
||||
"CRA-AI-1": {"nist": ["CM-6", "CM-7"], "owasp": ["A05:2021"]},
|
||||
"CRA-AI-2": {"nist": ["CM-7", "SC-7"], "owasp": ["A05:2021"]},
|
||||
"CRA-AI-3": {"nist": ["SA-8", "SC-7", "SC-39"], "owasp": ["A04:2021"]},
|
||||
"CRA-AI-4": {"nist": ["AC-6"], "owasp": ["A01:2021"]},
|
||||
"CRA-AI-5": {"nist": ["SI-7", "CM-5"], "owasp": ["A08:2021"]},
|
||||
"CRA-AI-6": {"nist": ["SI-7"], "owasp": ["A08:2021"]},
|
||||
"CRA-AI-7": {"nist": ["IA-2", "IA-2(1)"], "owasp": ["A07:2021"]},
|
||||
"CRA-AI-8": {"nist": ["IA-5", "IA-5(1)"], "owasp": ["A07:2021"]},
|
||||
"CRA-AI-9": {"nist": ["IA-5"], "owasp": ["A07:2021"]},
|
||||
"CRA-AI-10": {"nist": ["AC-12", "SC-23"], "owasp": ["A07:2021"]},
|
||||
"CRA-AI-11": {"nist": ["AC-7"], "owasp": ["A07:2021"]},
|
||||
"CRA-AI-12": {"nist": ["AC-2", "AC-3", "AC-6"], "owasp": ["A01:2021"]},
|
||||
"CRA-AI-13": {"nist": ["SC-13", "SC-28"], "owasp": ["A02:2021"]},
|
||||
"CRA-AI-14": {"nist": ["SC-28"], "owasp": ["A02:2021"]},
|
||||
"CRA-AI-15": {"nist": ["SC-8", "SC-8(1)"], "owasp": ["A02:2021"]},
|
||||
"CRA-AI-16": {"nist": ["SC-12"], "owasp": ["A02:2021"]},
|
||||
"CRA-AI-17": {"nist": ["SI-12"], "owasp": []},
|
||||
"CRA-AI-18": {"nist": ["SA-3", "SA-8", "SA-15"], "owasp": ["A04:2021"]},
|
||||
"CRA-AI-19": {"nist": ["SA-11", "SA-15"], "owasp": ["A04:2021"]},
|
||||
"CRA-AI-20": {"nist": ["SA-11", "SA-11(1)"], "owasp": ["A04:2021"]},
|
||||
"CRA-AI-21": {"nist": ["SR-3", "SR-5", "SR-6"], "owasp": ["A06:2021"]},
|
||||
"CRA-AI-22": {"nist": ["RA-5", "SI-2", "SR-4"], "owasp": ["A06:2021"]},
|
||||
"CRA-AI-23": {"nist": ["SR-4", "SR-3"], "owasp": ["A06:2021"]},
|
||||
"CRA-AI-24": {"nist": ["AU-2", "AU-3"], "owasp": ["A09:2021"]},
|
||||
"CRA-AI-25": {"nist": ["AU-6", "SI-4"], "owasp": ["A09:2021"]},
|
||||
"CRA-AI-26": {"nist": ["SI-4"], "owasp": ["A09:2021"]},
|
||||
"CRA-AI-27": {"nist": ["AU-9"], "owasp": ["A09:2021"]},
|
||||
"CRA-AI-28": {"nist": ["SI-2", "SI-7"], "owasp": ["A08:2021"]},
|
||||
"CRA-AI-29": {"nist": ["SI-7(15)", "SC-12"], "owasp": ["A08:2021"]},
|
||||
"CRA-AI-30": {"nist": ["SI-7"], "owasp": ["A08:2021"]},
|
||||
"CRA-AI-31": {"nist": ["SA-22"], "owasp": ["A06:2021"]},
|
||||
"CRA-AI-32": {"nist": ["RA-5"], "owasp": ["A06:2021"]},
|
||||
"CRA-AI-33": {"nist": ["RA-5", "SR-4"], "owasp": ["A06:2021"]},
|
||||
"CRA-AI-34": {"nist": ["RA-5", "RA-7"], "owasp": []},
|
||||
"CRA-AI-35": {"nist": ["SI-5"], "owasp": []},
|
||||
"CRA-AI-36": {"nist": ["IR-4", "IR-6"], "owasp": []},
|
||||
"CRA-AI-37": {"nist": ["IR-6"], "owasp": []},
|
||||
"CRA-AI-38": {"nist": ["IR-6", "IR-8"], "owasp": []},
|
||||
"CRA-AI-39": {"nist": ["SI-2"], "owasp": ["A06:2021"]},
|
||||
"CRA-AI-40": {"nist": ["IR-5", "AU-11"], "owasp": []},
|
||||
}
|
||||
|
||||
|
||||
def security_refs_for(req_ids: list) -> dict:
|
||||
"""Union of NIST + OWASP refs across the given CRA-AI requirement ids.
|
||||
|
||||
Returns {"nist": [...], "owasp": [{"code": .., "label": ..}]}, deduped,
|
||||
order-stable. The golden set is the single source — no retrieval.
|
||||
"""
|
||||
nist: list = []
|
||||
owasp: list = []
|
||||
seen_owasp = set()
|
||||
for rid in req_ids:
|
||||
entry = CRA_SECURITY_CROSSWALK.get(rid)
|
||||
if not entry:
|
||||
continue
|
||||
for n in entry["nist"]:
|
||||
if n not in nist:
|
||||
nist.append(n)
|
||||
for code in entry["owasp"]:
|
||||
if code not in seen_owasp:
|
||||
seen_owasp.add(code)
|
||||
owasp.append({"code": code, "label": OWASP_2021.get(code, "")})
|
||||
return {"nist": nist, "owasp": owasp}
|
||||
@@ -2,6 +2,8 @@
|
||||
from compliance.services.cra_finding_mapper import (
|
||||
ScannerFinding, map_finding, assess_findings, assess_findings_payload,
|
||||
)
|
||||
from compliance.services.cra_security_crosswalk import CRA_SECURITY_CROSSWALK
|
||||
from compliance.api.cra_annex_i_data import ANNEX_I_REQUIREMENTS
|
||||
|
||||
|
||||
def test_hardcoded_credentials_cwe_maps_to_credential_requirement():
|
||||
@@ -82,3 +84,31 @@ def test_empty_payload_is_safe():
|
||||
r = assess_findings_payload({})
|
||||
assert r["findings_total"] == 0
|
||||
assert r["coverage_pct"] == 0.0
|
||||
|
||||
|
||||
# --- Security golden-set crosswalk (NIST 800-53 + OWASP Top 10:2021) ---
|
||||
|
||||
def test_default_password_carries_nist_and_owasp_refs():
|
||||
m = map_finding(ScannerFinding(id="g1", title="default password", cwe="CWE-259", severity="high"))
|
||||
assert "IA-5" in m.nist_refs
|
||||
assert any(o["code"] == "A07:2021" for o in m.owasp_refs)
|
||||
|
||||
|
||||
def test_dependency_finding_maps_to_owasp_a06_and_nist_ra5():
|
||||
m = map_finding(ScannerFinding(id="g2", title="outdated dependency", category="dependency", severity="high"))
|
||||
assert "RA-5" in m.nist_refs
|
||||
assert any(o["code"] == "A06:2021" for o in m.owasp_refs)
|
||||
|
||||
|
||||
def test_unmapped_finding_has_no_security_refs():
|
||||
m = map_finding(ScannerFinding(id="g3", title="zzz nothing", severity="low"))
|
||||
assert m.nist_refs == [] and m.owasp_refs == []
|
||||
|
||||
|
||||
def test_golden_set_covers_every_requirement():
|
||||
# Completeness invariant: every CRA-AI requirement has a crosswalk entry
|
||||
# with at least one NIST control id (OWASP may be empty for process reqs).
|
||||
for req in ANNEX_I_REQUIREMENTS:
|
||||
rid = req["req_id"]
|
||||
assert rid in CRA_SECURITY_CROSSWALK, "missing crosswalk for {}".format(rid)
|
||||
assert CRA_SECURITY_CROSSWALK[rid]["nist"], "no NIST refs for {}".format(rid)
|
||||
|
||||
Reference in New Issue
Block a user