feat(cra): readiness derives obligations from Machinery Reg 2023/1230 too

Machine/plant builders are hit by BOTH the CRA and the new Machinery Regulation.
New machinery_reg_cyber.py models its two well-corroborated Annex III cyber-with-
safety essential requirements (1.1.9 protection against corruption, 1.2.1 control-
system safety incl. foreseeable manipulation) in our own words; EU legal text is
freely reusable (Commission Decision 2011/833/EU, source acknowledged), harmonised
standards referenced by identifier only. The readiness check asks "is it
machinery?" and, if so, adds these obligations tagged "Maschinen-VO" alongside the
CRA ones — the combination is visible (regulations list + per-item source badge).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Benjamin Admin
2026-06-14 14:26:08 +02:00
parent 2da3e5fdbb
commit b0f78ae9a3
4 changed files with 89 additions and 0 deletions
@@ -10,12 +10,14 @@ interface GuidelineItem {
severity: string severity: string
effort_days?: number effort_days?: number
measures: { id: string; name: string }[] measures: { id: string; name: string }[]
source?: string
} }
interface ReadinessResult { interface ReadinessResult {
in_scope: boolean in_scope: boolean
classification: string classification: string
rationale: string[] rationale: string[]
conformity_path_hint: string conformity_path_hint: string
regulations: string[]
guideline: { code: GuidelineItem[]; process: GuidelineItem[]; document: GuidelineItem[] } guideline: { code: GuidelineItem[]; process: GuidelineItem[]; document: GuidelineItem[] }
counts: { code: number; process: number; document: number } counts: { code: number; process: number; document: number }
total_effort_days: number total_effort_days: number
@@ -52,6 +54,7 @@ export function ReadinessCheck({ onCreateProject }: { onCreateProject?: () => vo
} }
const QUESTIONS: { k: string; label: string }[] = [ const QUESTIONS: { k: string; label: string }[] = [
{ k: 'is_machinery', label: 'Ist es eine Maschine/Anlage (CE nach Maschinenrecht)?' },
{ k: 'connected_to_internet', label: 'Hängt das Produkt am Internet (oder soll es)?' }, { k: 'connected_to_internet', label: 'Hängt das Produkt am Internet (oder soll es)?' },
{ k: 'user_parameter_app', label: 'Gibt es eine App, mit der Nutzer Parameter einstellen?' }, { k: 'user_parameter_app', label: 'Gibt es eine App, mit der Nutzer Parameter einstellen?' },
{ k: 'remote_maintenance', label: 'Bietet ihr Fernwartung an?' }, { k: 'remote_maintenance', label: 'Bietet ihr Fernwartung an?' },
@@ -110,6 +113,12 @@ export function ReadinessCheck({ onCreateProject }: { onCreateProject?: () => vo
</span> </span>
<span className="text-xs text-gray-500">· Konformität: {result.conformity_path_hint}</span> <span className="text-xs text-gray-500">· Konformität: {result.conformity_path_hint}</span>
</div> </div>
<div className="flex flex-wrap items-center gap-1 mb-1">
<span className="text-xs text-gray-500">Betroffene Verordnungen:</span>
{result.regulations.map((r) => (
<span key={r} className="rounded px-1.5 py-0.5 text-[10px] font-medium bg-blue-100 text-blue-700 dark:bg-blue-900/40 dark:text-blue-300">{r}</span>
))}
</div>
<p className="text-xs text-gray-500 mb-3"> <p className="text-xs text-gray-500 mb-3">
{result.counts.code + result.counts.process + result.counts.document} Pflichten · grobe Schätzung {result.counts.code + result.counts.process + result.counts.document} Pflichten · grobe Schätzung
~{result.total_effort_days} Personentage. Das ist ein Überblick zur Klärung keine Rechtsberatung. ~{result.total_effort_days} Personentage. Das ist ein Überblick zur Klärung keine Rechtsberatung.
@@ -124,6 +133,9 @@ export function ReadinessCheck({ onCreateProject }: { onCreateProject?: () => vo
<ul className="mt-2 space-y-1.5"> <ul className="mt-2 space-y-1.5">
{result.guideline[b.key].map((it) => ( {result.guideline[b.key].map((it) => (
<li key={it.req_id} className="text-[11px] text-gray-600 dark:text-gray-300"> <li key={it.req_id} className="text-[11px] text-gray-600 dark:text-gray-300">
{it.source === 'Maschinen-VO' && (
<span className="inline-block rounded bg-indigo-100 text-indigo-700 dark:bg-indigo-900/40 dark:text-indigo-300 px-1 py-0.5 text-[9px] font-medium mr-1">MaschVO</span>
)}
<span className="font-medium text-gray-800 dark:text-gray-200">{it.title}</span> <span className="font-medium text-gray-800 dark:text-gray-200">{it.title}</span>
<span className="text-gray-400"> · {it.annex_anchor}</span> <span className="text-gray-400"> · {it.annex_anchor}</span>
{it.measures.length > 0 && ( {it.measures.length > 0 && (
@@ -20,6 +20,7 @@ from compliance.services.cra_use_case_controls import enrich_findings_with_bread
from compliance.services.cra_component_findings import findings_from_components from compliance.services.cra_component_findings import findings_from_components
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.api.cra_routes import _classify # reuse the deterministic Annex III/IV classifier from compliance.api.cra_routes import _classify # reuse the deterministic Annex III/IV classifier
from compliance.api.machinery_reg_cyber import MACHINERY_REG_CYBER
from database import SessionLocal from database import SessionLocal
from .tenant_utils import get_tenant_id from .tenant_utils import get_tenant_id
@@ -128,6 +129,7 @@ class ReadinessRequest(BaseModel):
has_firmware: Optional[bool] = False has_firmware: Optional[bool] = False
remote_maintenance: Optional[bool] = False # implies connectivity + updates remote_maintenance: Optional[bool] = False # implies connectivity + updates
user_parameter_app: Optional[bool] = False # implies connectivity + updates user_parameter_app: Optional[bool] = False # implies connectivity + updates
is_machinery: Optional[bool] = False # CE machinery -> also Machinery Reg 2023/1230
# CRA Annex I evidence_type -> guideline bucket (Code / Prozess / Dokumentation). # CRA Annex I evidence_type -> guideline bucket (Code / Prozess / Dokumentation).
@@ -156,7 +158,9 @@ async def readiness(body: ReadinessRequest):
classification, rationale = _classify(intake) classification, rationale = _classify(intake)
in_scope = classification != "NOT_IN_SCOPE" in_scope = classification != "NOT_IN_SCOPE"
groups = {"code": [], "process": [], "document": []} groups = {"code": [], "process": [], "document": []}
regulations = []
if in_scope: if in_scope:
regulations.append("CRA")
for req in ANNEX_I_REQUIREMENTS: for req in ANNEX_I_REQUIREMENTS:
bucket = _GUIDELINE_BUCKET.get(req.get("evidence_type", "process"), "process") bucket = _GUIDELINE_BUCKET.get(req.get("evidence_type", "process"), "process")
groups[bucket].append({ groups[bucket].append({
@@ -164,6 +168,18 @@ async def readiness(body: ReadinessRequest):
"annex_anchor": req["annex_anchor"], "severity": req["severity"], "annex_anchor": req["annex_anchor"], "severity": req["severity"],
"effort_days": req.get("effort_days"), "effort_days": req.get("effort_days"),
"measures": [{"id": m, "name": MEASURES.get(m, m)} for m in req.get("mapped_measures", [])], "measures": [{"id": m, "name": MEASURES.get(m, m)} for m in req.get("mapped_measures", [])],
"source": "CRA",
})
# Machine/plant builders are ALSO hit by the new Machinery Regulation's
# cyber-with-safety essential requirements (Annex III) — show the combination.
if body.is_machinery:
regulations.append("Maschinen-VO 2023/1230")
for req in MACHINERY_REG_CYBER:
bucket = _GUIDELINE_BUCKET.get(req.get("evidence_type", "process"), "process")
groups[bucket].append({
"req_id": req["req_id"], "title": req["title"], "category": req["category"],
"annex_anchor": req["annex_anchor"], "severity": req["severity"],
"effort_days": None, "measures": [], "source": "Maschinen-VO",
}) })
total_effort = sum(r["effort_days"] for g in groups.values() for r in g if r.get("effort_days")) total_effort = sum(r["effort_days"] for g in groups.values() for r in g if r.get("effort_days"))
return { return {
@@ -171,6 +187,7 @@ async def readiness(body: ReadinessRequest):
"classification": classification, "classification": classification,
"rationale": rationale, "rationale": rationale,
"conformity_path_hint": _PATH_HINT.get(classification, ""), "conformity_path_hint": _PATH_HINT.get(classification, ""),
"regulations": regulations,
"guideline": groups, "guideline": groups,
"counts": {k: len(v) for k, v in groups.items()}, "counts": {k: len(v) for k, v in groups.items()},
"total_effort_days": total_effort, "total_effort_days": total_effort,
@@ -0,0 +1,50 @@
"""Machinery Regulation (EU) 2023/1230 — the NEW cyber-with-safety essential
requirements (Annex III). Applies from 20 Jan 2027 and, for the first time,
puts cybersecurity-affecting-safety into the CE machinery framework — the
counterpart to the CRA for machine/plant builders.
Own-words summaries of the regulation text (EU legal texts are freely reusable
incl. commercial with source acknowledgement, Commission Decision 2011/833/EU).
We do NOT reproduce verbatim, and harmonised standards (prEN 50742, EN ISO 13849,
EN ISO 12100) are referenced BY IDENTIFIER ONLY — they are copyrighted (CEN/ISO).
Scope note: only the two well-corroborated Annex III cyber clauses (1.1.9, 1.2.1)
are modelled. Further clauses are intentionally omitted rather than guessed.
"""
SOURCE_REGULATION = "Maschinenverordnung (EU) 2023/1230"
MACHINERY_REG_CYBER = [
{
"req_id": "MR-1.1.9",
"annex_anchor": "Anhang III, 1.1.9",
"title": "Schutz vor Korruption/Manipulation",
"category": "Manipulationsschutz",
"description": (
"Vernetzung oder Fernzugriff darf keine gefaehrliche Situation ausloesen. "
"Sicherheitsrelevante Hardware, Software und uebertragene Signale muessen identifiziert und "
"gegen versehentliche wie absichtliche Manipulation geschuetzt werden; Eingriffe in "
"sicherheitsrelevante Software/Konfiguration muessen als legitim oder unzulaessig "
"nachvollziehbar (protokolliert) sein."
),
"severity": "HIGH",
"evidence_type": "hybrid",
"norm_references": ["Maschinenverordnung (EU) 2023/1230, Anhang III, 1.1.9", "prEN 50742 (Entwurf)"],
"source_regulation": SOURCE_REGULATION,
},
{
"req_id": "MR-1.2.1",
"annex_anchor": "Anhang III, 1.2.1",
"title": "Sicherheit und Zuverlaessigkeit der Steuerungen",
"category": "Steuerungssicherheit",
"description": (
"Steuerungen sind so auszulegen, dass Fehler, aeussere Einfluesse (auch Funkstoerungen), "
"Bedienfehler und vernuenftigerweise vorhersehbare Manipulationsversuche Dritter zu keiner "
"gefaehrlichen Situation fuehren — die Sicherheit der Sicherheitsfunktionen muss erhalten bleiben."
),
"severity": "HIGH",
"evidence_type": "code",
"norm_references": ["Maschinenverordnung (EU) 2023/1230, Anhang III, 1.2.1", "EN ISO 13849", "prEN 50742 (Entwurf)"],
"source_regulation": SOURCE_REGULATION,
},
]
@@ -29,3 +29,13 @@ def test_no_digital_element_not_in_scope():
assert d["in_scope"] is False assert d["in_scope"] is False
assert d["classification"] == "NOT_IN_SCOPE" assert d["classification"] == "NOT_IN_SCOPE"
assert d["counts"]["code"] == 0 assert d["counts"]["code"] == 0
def test_machinery_adds_tagged_machinery_reg_obligations():
d = client.post("/api/v1/cra/readiness", json={
"intended_use": "App fuer Industrieanlagen", "connected_to_internet": True, "is_machinery": True}).json()
assert "Maschinen-VO 2023/1230" in d["regulations"]
items = d["guideline"]["code"] + d["guideline"]["process"] + d["guideline"]["document"]
assert any(it["source"] == "Maschinen-VO" for it in items)
assert any(it["req_id"] == "MR-1.1.9" for it in items)
assert any(it["source"] == "CRA" for it in items)