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:
@@ -92,7 +92,7 @@ export function CRACyberView({ data }: { data: CRADemo }) {
|
|||||||
<tr className="text-gray-500 border-b border-gray-200 dark:border-gray-700 text-left">
|
<tr className="text-gray-500 border-b border-gray-200 dark:border-gray-700 text-left">
|
||||||
<th className="py-2 px-4">Cyber-Befund</th>
|
<th className="py-2 px-4">Cyber-Befund</th>
|
||||||
<th className="py-2 px-3">CRA-Anforderung</th>
|
<th className="py-2 px-3">CRA-Anforderung</th>
|
||||||
<th className="py-2 px-3">Annex I</th>
|
<th className="py-2 px-3">Best Practice (NIST / OWASP)</th>
|
||||||
<th className="py-2 px-3">Risiko</th>
|
<th className="py-2 px-3">Risiko</th>
|
||||||
<th className="py-2 px-4">Maßnahmen</th>
|
<th className="py-2 px-4">Maßnahmen</th>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -109,11 +109,25 @@ export function CRACyberView({ data }: { data: CRADemo }) {
|
|||||||
{f.requirement_ids.length > 1 && (
|
{f.requirement_ids.length > 1 && (
|
||||||
<span className="text-[10px] text-gray-400"> +{f.requirement_ids.length - 1}</span>
|
<span className="text-[10px] text-gray-400"> +{f.requirement_ids.length - 1}</span>
|
||||||
)}
|
)}
|
||||||
|
<div className="text-[10px] text-gray-400">{f.annex_anchor}</div>
|
||||||
|
</td>
|
||||||
|
<td className="py-2 px-3">
|
||||||
|
<div className="flex flex-wrap gap-1">
|
||||||
|
{f.nist_refs.map((n) => (
|
||||||
|
<span key={n} className="inline-block rounded bg-slate-100 text-slate-600 dark:bg-slate-700 dark:text-slate-300 px-1.5 py-0.5 text-[10px] font-mono">
|
||||||
|
{n}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
{f.owasp_refs.map((o) => (
|
||||||
|
<span key={o.code} title={o.label} className="inline-block rounded bg-blue-100 text-blue-700 dark:bg-blue-900/40 dark:text-blue-300 px-1.5 py-0.5 text-[10px] font-medium">
|
||||||
|
{o.code}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td className="py-2 px-3 text-gray-500">{f.annex_anchor}</td>
|
|
||||||
<td className="py-2 px-3"><RiskBadge level={f.risk_level} /></td>
|
<td className="py-2 px-3"><RiskBadge level={f.risk_level} /></td>
|
||||||
<td className="py-2 px-4 text-gray-600 dark:text-gray-300">
|
<td className="py-2 px-4 text-gray-600 dark:text-gray-300">
|
||||||
{f.measures.length ? f.measures.map((me) => me.id).join(', ') : <span className="text-gray-400">—</span>}
|
{f.measures.length ? f.measures.join(', ') : <span className="text-gray-400">—</span>}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
@@ -122,29 +136,40 @@ export function CRACyberView({ data }: { data: CRADemo }) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Measures + deadlines */}
|
{/* Recommended measures — full curated text + norm references */}
|
||||||
<div className="grid md:grid-cols-2 gap-3">
|
<div className="rounded-xl border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 p-4">
|
||||||
<div className="rounded-xl border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 p-4">
|
<h2 className="text-sm font-semibold text-gray-800 dark:text-gray-200 mb-1">Empfohlene Maßnahmen</h2>
|
||||||
<h2 className="text-sm font-semibold text-gray-800 dark:text-gray-200 mb-2">Empfohlene Maßnahmen</h2>
|
<p className="text-[11px] text-gray-400 mb-3">Kuratierte CRA-Maßnahmen aus der BreakPilot-Bibliothek — mit Normverweisen.</p>
|
||||||
<ul className="space-y-1.5">
|
<div className="space-y-3">
|
||||||
{data.open_measures.map((me) => (
|
{data.open_measures.map((me) => (
|
||||||
<li key={me.id} className="text-xs text-gray-600 dark:text-gray-300">
|
<div key={me.id} className="rounded-lg border border-gray-100 dark:border-gray-700/60 p-3">
|
||||||
<span className="font-medium text-gray-700 dark:text-gray-200">{me.id}</span> — {me.description}
|
<p className="text-sm font-medium text-gray-800 dark:text-gray-200">
|
||||||
</li>
|
<span className="font-mono text-purple-600 dark:text-purple-400">{me.id}</span> — {me.name}
|
||||||
))}
|
</p>
|
||||||
</ul>
|
<p className="text-xs text-gray-600 dark:text-gray-300 mt-1">{me.description}</p>
|
||||||
</div>
|
<div className="flex flex-wrap gap-1 mt-2">
|
||||||
<div className="rounded-xl border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 p-4">
|
{me.norm_refs.map((nr) => (
|
||||||
<h2 className="text-sm font-semibold text-gray-800 dark:text-gray-200 mb-2">CRA-Fristen</h2>
|
<span key={nr} className="inline-block rounded bg-gray-100 text-gray-500 dark:bg-gray-700 dark:text-gray-300 px-1.5 py-0.5 text-[10px]">
|
||||||
<ul className="space-y-1.5">
|
{nr}
|
||||||
{data.deadlines.map((d) => (
|
</span>
|
||||||
<li key={d.date} className="text-xs text-gray-600 dark:text-gray-300 flex gap-2">
|
))}
|
||||||
<span className="font-mono text-gray-500">{d.date}</span> {d.label}
|
</div>
|
||||||
</li>
|
</div>
|
||||||
))}
|
))}
|
||||||
</ul>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* CRA deadlines */}
|
||||||
|
<div className="rounded-xl border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 p-4">
|
||||||
|
<h2 className="text-sm font-semibold text-gray-800 dark:text-gray-200 mb-2">CRA-Fristen</h2>
|
||||||
|
<ul className="flex flex-wrap gap-4">
|
||||||
|
{data.deadlines.map((d) => (
|
||||||
|
<li key={d.date} className="text-xs text-gray-600 dark:text-gray-300 flex items-center gap-2">
|
||||||
|
<span className="font-mono text-gray-500">{d.date}</span> {d.label}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,11 +2,15 @@
|
|||||||
|
|
||||||
// DEMO data layer for the CRA / Cyber tab. The Kistenhubgeraet (crate lift) CE
|
// DEMO data layer for the CRA / Cyber tab. The Kistenhubgeraet (crate lift) CE
|
||||||
// project is treated AS IF it had an internet-connected IoT module. The cyber
|
// project is treated AS IF it had an internet-connected IoT module. The cyber
|
||||||
// findings are invented, but the CRA mapping below is the REAL output of the
|
// findings are invented, but the CRA mapping + the NIST 800-53 / OWASP Top 10
|
||||||
// deterministic backend mapper (compliance/services/cra_finding_mapper.py) run
|
// crosswalk below are the REAL output of the deterministic backend
|
||||||
// on these findings — so the integration concept is faithful. The cross_links
|
// (compliance/services/cra_finding_mapper.py + cra_security_crosswalk.py) run on
|
||||||
// (cyber re-opens a mechanically-mitigated safety hazard) are the core idea we
|
// these findings — so the integration concept is faithful. Measure texts +
|
||||||
// want to validate visually. Live wiring replaces this fixture later.
|
// norm references are the real curated entries from measures_library_cra.go.
|
||||||
|
// The cross_links (cyber re-opens a mechanically-mitigated safety hazard) are
|
||||||
|
// the core idea we validate visually. Live wiring replaces this fixture later.
|
||||||
|
|
||||||
|
export interface OwaspRef { code: string; label: string }
|
||||||
|
|
||||||
export interface CRAFinding {
|
export interface CRAFinding {
|
||||||
id: string
|
id: string
|
||||||
@@ -19,8 +23,17 @@ export interface CRAFinding {
|
|||||||
requirement_ids: string[]
|
requirement_ids: string[]
|
||||||
annex_anchor: string
|
annex_anchor: string
|
||||||
iso27001_ref: string[]
|
iso27001_ref: string[]
|
||||||
|
nist_refs: string[]
|
||||||
|
owasp_refs: OwaspRef[]
|
||||||
risk_level: string
|
risk_level: string
|
||||||
measures: { id: string; description: string }[]
|
measures: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Measure {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
description: string
|
||||||
|
norm_refs: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CrossLink {
|
export interface CrossLink {
|
||||||
@@ -38,19 +51,27 @@ export interface CRADemo {
|
|||||||
by_risk: Record<string, number>
|
by_risk: Record<string, number>
|
||||||
coverage_pct: number
|
coverage_pct: number
|
||||||
requirements_touched: string[]
|
requirements_touched: string[]
|
||||||
open_measures: { id: string; description: string }[]
|
open_measures: Measure[]
|
||||||
cross_links: CrossLink[]
|
cross_links: CrossLink[]
|
||||||
deadlines: { date: string; label: string }[]
|
deadlines: { date: string; label: string }[]
|
||||||
}
|
}
|
||||||
|
|
||||||
const MEASURE_DESC: Record<string, string> = {
|
const ow = (code: string, label: string): OwaspRef => ({ code, label })
|
||||||
M541: 'Signierte Software- und Firmware-Updates mit Rollback-Schutz',
|
|
||||||
M542: 'Initiale Default-Passwoerter beim ersten Start erzwungen aendern',
|
|
||||||
M545: 'Cybersecurity-Hardening-Guide fuer den Anwender beilegen',
|
|
||||||
M547: 'Updates ueber authentisierten Kanal mit Integritaetspruefung',
|
|
||||||
}
|
|
||||||
|
|
||||||
const m = (...ids: string[]) => ids.map((id) => ({ id, description: MEASURE_DESC[id] || '' }))
|
const MEASURES: Measure[] = [
|
||||||
|
{ id: 'M542', name: 'Initiale Default-Passwoerter beim ersten Start erzwungen aendern',
|
||||||
|
description: 'Die Maschine fordert beim ersten Hochfahren zwingend die Aenderung aller werkseitigen Default-Passwoerter (Bediener, Wartung, Admin). Default-Credentials werden nicht in Klartext in der Dokumentation veroeffentlicht, sondern dem Betreiber separat (versiegelt) uebergeben. Eine Wiederherstellung auf Default-Credentials ist nur ueber physischen Zugriff moeglich.',
|
||||||
|
norm_refs: ['Verordnung (EU) 2024/2847 (CRA), Anhang I', 'DIN EN 40000-1-2 (Entwurf)', 'ETSI EN 303 645'] },
|
||||||
|
{ id: 'M547', name: 'Updates ueber authentisierten Kanal mit Integritaetspruefung',
|
||||||
|
description: 'Der Update-Kanal (Online-Pull oder USB-Push) ist gegen Manipulation gesichert: TLS-1.3 mit Zertifikatspruefung bei Online-Updates, Hash-Pruefung der Update-Datei vor dem Anwenden. Der Update-Prozess ist atomar: bei Abbruch bleibt die alte Version lauffaehig.',
|
||||||
|
norm_refs: ['Verordnung (EU) 2024/2847 (CRA), Anhang I', 'DIN EN 40000-1-2 (Entwurf)', 'IEC 62443-4-2'] },
|
||||||
|
{ id: 'M541', name: 'Signierte Software- und Firmware-Updates mit Rollback-Schutz',
|
||||||
|
description: 'Updates der Steuerungs-, Roboter- und Visualisierungssoftware werden ausschliesslich kryptographisch signiert ausgeliefert. Die Steuerung prueft die Signatur vor der Installation und verweigert das Einspielen unsignierter Pakete. Ein Rollback-Schutz verhindert das Downgraden auf nachweislich verwundbare Versionen.',
|
||||||
|
norm_refs: ['Verordnung (EU) 2024/2847 (CRA), Anhang I', 'DIN EN 40000-1-2 (Entwurf)', 'IEC 62443-4-1'] },
|
||||||
|
{ id: 'M545', name: 'Cybersecurity-Hardening-Guide fuer den Anwender beilegen',
|
||||||
|
description: 'Die mit der Maschine ausgelieferte Dokumentation enthaelt einen Cybersecurity-Hardening-Guide: empfohlene Netzwerk-Segmentierung, deaktivierbare Dienste, sichere Konfiguration aller eingebauten Komponenten (PLC, HMI, Roboter), Empfehlungen zur Benutzer-Verwaltung. Der Guide wird bei Update-Releases gepflegt.',
|
||||||
|
norm_refs: ['Verordnung (EU) 2024/2847 (CRA), Anhang II', 'DIN EN 40000-1-2 (Entwurf)', 'IEC 62443-3-3'] },
|
||||||
|
]
|
||||||
|
|
||||||
const DEMO: CRADemo = {
|
const DEMO: CRADemo = {
|
||||||
scenario:
|
scenario:
|
||||||
@@ -59,32 +80,38 @@ const DEMO: CRADemo = {
|
|||||||
{ id: 'KH-CY-1', title: 'Fernsteuer-Weboberflaeche mit universellem Default-Passwort', location: 'remote-ui/login',
|
{ id: 'KH-CY-1', title: 'Fernsteuer-Weboberflaeche mit universellem Default-Passwort', location: 'remote-ui/login',
|
||||||
scanner_severity: 'critical', cwe: 'CWE-259', primary_requirement: 'CRA-AI-8',
|
scanner_severity: 'critical', cwe: 'CWE-259', primary_requirement: 'CRA-AI-8',
|
||||||
requirement_title: 'Keine Default-Passwoerter', requirement_ids: ['CRA-AI-8'],
|
requirement_title: 'Keine Default-Passwoerter', requirement_ids: ['CRA-AI-8'],
|
||||||
annex_anchor: 'Annex I, 1(3)(d)', iso27001_ref: ['A.8.5'], risk_level: 'CRITICAL', measures: m('M542') },
|
annex_anchor: 'Annex I, 1(3)(d)', iso27001_ref: ['A.8.5'], nist_refs: ['IA-5', 'IA-5(1)'],
|
||||||
|
owasp_refs: [ow('A07:2021', 'Identification and Authentication Failures')], risk_level: 'CRITICAL', measures: ['M542'] },
|
||||||
{ id: 'KH-CY-2', title: 'IoT-Telemetrie unverschluesselt ueber MQTT', location: 'telemetry/mqtt',
|
{ id: 'KH-CY-2', title: 'IoT-Telemetrie unverschluesselt ueber MQTT', location: 'telemetry/mqtt',
|
||||||
scanner_severity: 'high', cwe: 'CWE-319', primary_requirement: 'CRA-AI-15',
|
scanner_severity: 'high', cwe: 'CWE-319', primary_requirement: 'CRA-AI-15',
|
||||||
requirement_title: 'Transport-Schutz (Data in Transit)', requirement_ids: ['CRA-AI-15', 'CRA-AI-13'],
|
requirement_title: 'Transport-Schutz (Data in Transit)', requirement_ids: ['CRA-AI-15', 'CRA-AI-13'],
|
||||||
annex_anchor: 'Annex I, 1(3)(e)', iso27001_ref: ['A.8.24'], risk_level: 'HIGH', measures: [] },
|
annex_anchor: 'Annex I, 1(3)(e)', iso27001_ref: ['A.8.24'], nist_refs: ['SC-8', 'SC-8(1)', 'SC-13', 'SC-28'],
|
||||||
|
owasp_refs: [ow('A02:2021', 'Cryptographic Failures')], risk_level: 'HIGH', measures: [] },
|
||||||
{ id: 'KH-CY-3', title: 'Firmware-Updates ohne Signaturpruefung', location: 'updater',
|
{ id: 'KH-CY-3', title: 'Firmware-Updates ohne Signaturpruefung', location: 'updater',
|
||||||
scanner_severity: 'high', cwe: 'CWE-494', primary_requirement: 'CRA-AI-30',
|
scanner_severity: 'high', cwe: 'CWE-494', primary_requirement: 'CRA-AI-30',
|
||||||
requirement_title: 'Update-Integritaet', requirement_ids: ['CRA-AI-30', 'CRA-AI-28', 'CRA-AI-6'],
|
requirement_title: 'Update-Integritaet', requirement_ids: ['CRA-AI-30', 'CRA-AI-28', 'CRA-AI-6'],
|
||||||
annex_anchor: 'Annex I, 1(4)', iso27001_ref: ['A.8.24'], risk_level: 'HIGH', measures: m('M547', 'M541') },
|
annex_anchor: 'Annex I, 1(4)', iso27001_ref: ['A.8.24'], nist_refs: ['SI-7', 'SI-2'],
|
||||||
|
owasp_refs: [ow('A08:2021', 'Software and Data Integrity Failures')], risk_level: 'HIGH', measures: ['M547', 'M541'] },
|
||||||
{ id: 'KH-CY-4', title: 'Offener Debug-Port (telnet) am Controller', location: 'controller:23',
|
{ id: 'KH-CY-4', title: 'Offener Debug-Port (telnet) am Controller', location: 'controller:23',
|
||||||
scanner_severity: 'medium', cwe: 'CWE-1188', primary_requirement: 'CRA-AI-1',
|
scanner_severity: 'medium', cwe: 'CWE-1188', primary_requirement: 'CRA-AI-1',
|
||||||
requirement_title: 'Secure-by-Default-Konfiguration', requirement_ids: ['CRA-AI-1'],
|
requirement_title: 'Secure-by-Default-Konfiguration', requirement_ids: ['CRA-AI-1'],
|
||||||
annex_anchor: 'Annex I, 1(1)', iso27001_ref: ['A.8.9'], risk_level: 'HIGH', measures: m('M545') },
|
annex_anchor: 'Annex I, 1(1)', iso27001_ref: ['A.8.9'], nist_refs: ['CM-6', 'CM-7'],
|
||||||
|
owasp_refs: [ow('A05:2021', 'Security Misconfiguration')], risk_level: 'HIGH', measures: ['M545'] },
|
||||||
{ id: 'KH-CY-5', title: 'Gebundelte libmodbus mit bekannter CVE (veraltet)', location: 'deps/libmodbus',
|
{ id: 'KH-CY-5', title: 'Gebundelte libmodbus mit bekannter CVE (veraltet)', location: 'deps/libmodbus',
|
||||||
scanner_severity: 'high', cwe: 'CWE-1104', primary_requirement: 'CRA-AI-22',
|
scanner_severity: 'high', cwe: 'CWE-1104', primary_requirement: 'CRA-AI-22',
|
||||||
requirement_title: 'Dependency-Monitoring', requirement_ids: ['CRA-AI-22'],
|
requirement_title: 'Dependency-Monitoring', requirement_ids: ['CRA-AI-22'],
|
||||||
annex_anchor: 'Annex I, 1(5)', iso27001_ref: ['A.8.8', 'A.8.25'], risk_level: 'HIGH', measures: [] },
|
annex_anchor: 'Annex I, 1(5)', iso27001_ref: ['A.8.8', 'A.8.25'], nist_refs: ['RA-5', 'SI-2', 'SR-4'],
|
||||||
|
owasp_refs: [ow('A06:2021', 'Vulnerable and Outdated Components')], risk_level: 'HIGH', measures: [] },
|
||||||
{ id: 'KH-CY-6', title: 'Keine Sicherheits-Protokollierung der Remote-Befehle', location: 'remote-ui',
|
{ id: 'KH-CY-6', title: 'Keine Sicherheits-Protokollierung der Remote-Befehle', location: 'remote-ui',
|
||||||
scanner_severity: 'medium', cwe: 'CWE-778', primary_requirement: 'CRA-AI-24',
|
scanner_severity: 'medium', cwe: 'CWE-778', primary_requirement: 'CRA-AI-24',
|
||||||
requirement_title: 'Security-Logging', requirement_ids: ['CRA-AI-24'],
|
requirement_title: 'Security-Logging', requirement_ids: ['CRA-AI-24'],
|
||||||
annex_anchor: 'Annex I, 1(3)(g)', iso27001_ref: ['A.8.15'], risk_level: 'MEDIUM', measures: [] },
|
annex_anchor: 'Annex I, 1(3)(g)', iso27001_ref: ['A.8.15'], nist_refs: ['AU-2', 'AU-3'],
|
||||||
|
owasp_refs: [ow('A09:2021', 'Security Logging and Monitoring Failures')], risk_level: 'MEDIUM', measures: [] },
|
||||||
],
|
],
|
||||||
by_risk: { CRITICAL: 1, HIGH: 4, MEDIUM: 1, LOW: 0 },
|
by_risk: { CRITICAL: 1, HIGH: 4, MEDIUM: 1, LOW: 0 },
|
||||||
coverage_pct: 100.0,
|
coverage_pct: 100.0,
|
||||||
requirements_touched: ['CRA-AI-1', 'CRA-AI-6', 'CRA-AI-8', 'CRA-AI-13', 'CRA-AI-15', 'CRA-AI-22', 'CRA-AI-24', 'CRA-AI-28', 'CRA-AI-30'],
|
requirements_touched: ['CRA-AI-1', 'CRA-AI-6', 'CRA-AI-8', 'CRA-AI-13', 'CRA-AI-15', 'CRA-AI-22', 'CRA-AI-24', 'CRA-AI-28', 'CRA-AI-30'],
|
||||||
open_measures: m('M542', 'M547', 'M541', 'M545'),
|
open_measures: MEASURES,
|
||||||
cross_links: [
|
cross_links: [
|
||||||
{
|
{
|
||||||
cyber_finding_ids: ['KH-CY-1', 'KH-CY-3'],
|
cyber_finding_ids: ['KH-CY-1', 'KH-CY-3'],
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ from dataclasses import dataclass, field, asdict
|
|||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from compliance.api.cra_annex_i_data import ANNEX_I_REQUIREMENTS, MEASURES, DEADLINES
|
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}
|
_REQ_INDEX = {r["req_id"]: r for r in ANNEX_I_REQUIREMENTS}
|
||||||
_SEV_ORDER = {"LOW": 1, "MEDIUM": 2, "HIGH": 3, "CRITICAL": 4}
|
_SEV_ORDER = {"LOW": 1, "MEDIUM": 2, "HIGH": 3, "CRITICAL": 4}
|
||||||
@@ -89,6 +90,8 @@ class MappedFinding:
|
|||||||
iso27001_ref: list = field(default_factory=list)
|
iso27001_ref: list = field(default_factory=list)
|
||||||
risk_level: str = "LOW"
|
risk_level: str = "LOW"
|
||||||
measures: list = field(default_factory=list)
|
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 = ""
|
rationale: str = ""
|
||||||
unmapped: bool = False
|
unmapped: bool = False
|
||||||
|
|
||||||
@@ -162,6 +165,7 @@ def map_finding(f: ScannerFinding) -> MappedFinding:
|
|||||||
for m in _REQ_INDEX[rid].get("mapped_measures", []):
|
for m in _REQ_INDEX[rid].get("mapped_measures", []):
|
||||||
if m not in measures:
|
if m not in measures:
|
||||||
measures.append(m)
|
measures.append(m)
|
||||||
|
refs = security_refs_for(reqs)
|
||||||
return MappedFinding(
|
return MappedFinding(
|
||||||
finding_id=f.id,
|
finding_id=f.id,
|
||||||
requirement_ids=reqs,
|
requirement_ids=reqs,
|
||||||
@@ -170,6 +174,8 @@ def map_finding(f: ScannerFinding) -> MappedFinding:
|
|||||||
iso27001_ref=list(primary.get("iso27001_ref", [])),
|
iso27001_ref=list(primary.get("iso27001_ref", [])),
|
||||||
risk_level=_SEV_BY_RANK.get(risk_rank, "LOW"),
|
risk_level=_SEV_BY_RANK.get(risk_rank, "LOW"),
|
||||||
measures=measures,
|
measures=measures,
|
||||||
|
nist_refs=refs["nist"],
|
||||||
|
owasp_refs=refs["owasp"],
|
||||||
rationale="{}: {}".format(primary["req_id"], primary.get("title", "")),
|
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 (
|
from compliance.services.cra_finding_mapper import (
|
||||||
ScannerFinding, map_finding, assess_findings, assess_findings_payload,
|
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():
|
def test_hardcoded_credentials_cwe_maps_to_credential_requirement():
|
||||||
@@ -82,3 +84,31 @@ def test_empty_payload_is_safe():
|
|||||||
r = assess_findings_payload({})
|
r = assess_findings_payload({})
|
||||||
assert r["findings_total"] == 0
|
assert r["findings_total"] == 0
|
||||||
assert r["coverage_pct"] == 0.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