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
|
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,13 +168,26 @@ 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 {
|
||||||
"in_scope": in_scope,
|
"in_scope": in_scope,
|
||||||
"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)
|
||||||
|
|||||||
Reference in New Issue
Block a user