diff --git a/admin-compliance/app/sdk/cra/_components/ReadinessCheck.tsx b/admin-compliance/app/sdk/cra/_components/ReadinessCheck.tsx
index 379f961d..00324f1d 100644
--- a/admin-compliance/app/sdk/cra/_components/ReadinessCheck.tsx
+++ b/admin-compliance/app/sdk/cra/_components/ReadinessCheck.tsx
@@ -10,12 +10,14 @@ interface GuidelineItem {
severity: string
effort_days?: number
measures: { id: string; name: string }[]
+ source?: string
}
interface ReadinessResult {
in_scope: boolean
classification: string
rationale: string[]
conformity_path_hint: string
+ regulations: string[]
guideline: { code: GuidelineItem[]; process: GuidelineItem[]; document: GuidelineItem[] }
counts: { code: number; process: number; document: number }
total_effort_days: number
@@ -52,6 +54,7 @@ export function ReadinessCheck({ onCreateProject }: { onCreateProject?: () => vo
}
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: 'user_parameter_app', label: 'Gibt es eine App, mit der Nutzer Parameter einstellen?' },
{ k: 'remote_maintenance', label: 'Bietet ihr Fernwartung an?' },
@@ -110,6 +113,12 @@ export function ReadinessCheck({ onCreateProject }: { onCreateProject?: () => vo
· Konformität: {result.conformity_path_hint}
+
+ Betroffene Verordnungen:
+ {result.regulations.map((r) => (
+ {r}
+ ))}
+
{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.
@@ -124,6 +133,9 @@ export function ReadinessCheck({ onCreateProject }: { onCreateProject?: () => vo
{result.guideline[b.key].map((it) => (
-
+ {it.source === 'Maschinen-VO' && (
+ MaschVO
+ )}
{it.title}
· {it.annex_anchor}
{it.measures.length > 0 && (
diff --git a/backend-compliance/compliance/api/cra_assess_routes.py b/backend-compliance/compliance/api/cra_assess_routes.py
index 2792dbeb..e6098a06 100644
--- a/backend-compliance/compliance/api/cra_assess_routes.py
+++ b/backend-compliance/compliance/api/cra_assess_routes.py
@@ -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.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.machinery_reg_cyber import MACHINERY_REG_CYBER
from database import SessionLocal
from .tenant_utils import get_tenant_id
@@ -128,6 +129,7 @@ class ReadinessRequest(BaseModel):
has_firmware: Optional[bool] = False
remote_maintenance: 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).
@@ -156,7 +158,9 @@ async def readiness(body: ReadinessRequest):
classification, rationale = _classify(intake)
in_scope = classification != "NOT_IN_SCOPE"
groups = {"code": [], "process": [], "document": []}
+ regulations = []
if in_scope:
+ regulations.append("CRA")
for req in ANNEX_I_REQUIREMENTS:
bucket = _GUIDELINE_BUCKET.get(req.get("evidence_type", "process"), "process")
groups[bucket].append({
@@ -164,13 +168,26 @@ async def readiness(body: ReadinessRequest):
"annex_anchor": req["annex_anchor"], "severity": req["severity"],
"effort_days": req.get("effort_days"),
"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"))
return {
"in_scope": in_scope,
"classification": classification,
"rationale": rationale,
"conformity_path_hint": _PATH_HINT.get(classification, ""),
+ "regulations": regulations,
"guideline": groups,
"counts": {k: len(v) for k, v in groups.items()},
"total_effort_days": total_effort,
diff --git a/backend-compliance/compliance/api/machinery_reg_cyber.py b/backend-compliance/compliance/api/machinery_reg_cyber.py
new file mode 100644
index 00000000..1886bc85
--- /dev/null
+++ b/backend-compliance/compliance/api/machinery_reg_cyber.py
@@ -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,
+ },
+]
diff --git a/backend-compliance/tests/test_cra_readiness.py b/backend-compliance/tests/test_cra_readiness.py
index ca7c66cc..4e150b44 100644
--- a/backend-compliance/tests/test_cra_readiness.py
+++ b/backend-compliance/tests/test_cra_readiness.py
@@ -29,3 +29,13 @@ def test_no_digital_element_not_in_scope():
assert d["in_scope"] is False
assert d["classification"] == "NOT_IN_SCOPE"
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)