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:
@@ -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
|
||||
</span>
|
||||
<span className="text-xs text-gray-500">· Konformität: {result.conformity_path_hint}</span>
|
||||
</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">
|
||||
{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
|
||||
<ul className="mt-2 space-y-1.5">
|
||||
{result.guideline[b.key].map((it) => (
|
||||
<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="text-gray-400"> · {it.annex_anchor}</span>
|
||||
{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.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,
|
||||
|
||||
@@ -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["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)
|
||||
|
||||
Reference in New Issue
Block a user