From 4f4ffc2ad5accbf9355ba75c204bd88564dd091d Mon Sep 17 00:00:00 2001 From: Benjamin Admin Date: Wed, 17 Jun 2026 09:18:23 +0200 Subject: [PATCH] feat(cra): Cyber-trifft-CE mit echten IACE-Safety-Functions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit useCRA leitet aus den Hazards+Mitigations DES IACE-Projekts cyber-relevante safety_functions ab (Bewegung/Quetschen/safety_function_failure/Pneumatik → prevent_unexpected_actuation; Signal/Sensor/Kommunikation → signal_integrity; rein physische wie Thermik/Ergonomie ausgeschlossen) und gibt sie statt der Demo-Hardcodes an /assess. build_cross_links zeigt dann, welche REALE Projekt-Schutzmassnahme ein Cyber-Befund wieder oeffnet. Fallback auf Demo-Set, bis die Projekt-Hazards geladen sind. Co-Authored-By: Claude Opus 4.7 --- .../cra/_hooks/iace-safety-bridge.ts | 85 +++++++++++++++++++ .../sdk/iace/[projectId]/cra/_hooks/useCRA.ts | 21 ++++- 2 files changed, 102 insertions(+), 4 deletions(-) create mode 100644 admin-compliance/app/sdk/iace/[projectId]/cra/_hooks/iace-safety-bridge.ts diff --git a/admin-compliance/app/sdk/iace/[projectId]/cra/_hooks/iace-safety-bridge.ts b/admin-compliance/app/sdk/iace/[projectId]/cra/_hooks/iace-safety-bridge.ts new file mode 100644 index 00000000..0c050897 --- /dev/null +++ b/admin-compliance/app/sdk/iace/[projectId]/cra/_hooks/iace-safety-bridge.ts @@ -0,0 +1,85 @@ +// Cyber-meets-CE: derive cyber-defeatable safety functions from an IACE project's +// real hazards + mitigations, to feed the CRA cross-link bridge (replaces the demo +// hardcode). Only control-dependent hazards (movement/actuation, signal-integrity) +// can be re-opened by a cyber attack — purely physical ones (thermal, sharp edges, +// noise) are excluded. Mirrors the backend cra_safety_bridge kinds. + +export interface IaceHazard { + id: string + name: string + description?: string + category: string + possible_harm?: string +} +export interface IaceMitigation { + id: string + name: string + reduction_type?: string + linked_hazard_ids?: string[] +} +export interface SafetyFunction { + name: string + hazard: string + original_measure: string + kind: string +} + +// IACE hazard category -> the cyber-bridge safety-function kind. +const KIND_BY_CATEGORY: Record = { + // control-driven movement/actuation: a cyber attack can trigger it + mechanical_hazard: 'prevent_unexpected_actuation', + mechanical: 'prevent_unexpected_actuation', + crush: 'prevent_unexpected_actuation', + crush_point: 'prevent_unexpected_actuation', + crushing: 'prevent_unexpected_actuation', + movement: 'prevent_unexpected_actuation', + pneumatic_hydraulic: 'prevent_unexpected_actuation', + safety_function_failure: 'prevent_unexpected_actuation', + software_control: 'prevent_unexpected_actuation', + software_fault: 'prevent_unexpected_actuation', + // sensor/signal/parameter integrity: a cyber attack can corrupt it + signal: 'signal_integrity', + signale: 'signal_integrity', + sensor_spoofing: 'signal_integrity', + communication_failure: 'signal_integrity', + configuration_error: 'signal_integrity', +} + +export function deriveSafetyFunctions(hazards: IaceHazard[], mitigations: IaceMitigation[]): SafetyFunction[] { + const measuresByHazard: Record = {} + for (const m of mitigations || []) { + for (const hid of m.linked_hazard_ids || []) { + (measuresByHazard[hid] ||= []).push(m.name) + } + } + const out: SafetyFunction[] = [] + for (const h of hazards || []) { + const kind = KIND_BY_CATEGORY[h.category] + if (!kind) continue + out.push({ + name: h.name, + hazard: h.possible_harm || h.description || h.name, + original_measure: (measuresByHazard[h.id] || []).join(', ') || 'IACE-Schutzmaßnahme', + kind, + }) + } + return out +} + +// Fetch the project's hazards + mitigations and derive the cyber-relevant safety +// functions. Returns [] on any error (caller falls back to its demo functions). +export async function fetchProjectSafetyFunctions(projectId: string): Promise { + try { + const [hRes, mRes] = await Promise.all([ + fetch(`/api/sdk/v1/iace/projects/${projectId}/hazards`), + fetch(`/api/sdk/v1/iace/projects/${projectId}/mitigations`), + ]) + const hJson = hRes.ok ? await hRes.json() : {} + const mJson = mRes.ok ? await mRes.json() : {} + const hazards: IaceHazard[] = hJson.hazards || (Array.isArray(hJson) ? hJson : []) + const mitigations: IaceMitigation[] = mJson.mitigations || (Array.isArray(mJson) ? mJson : []) + return deriveSafetyFunctions(hazards, mitigations) + } catch { + return [] + } +} diff --git a/admin-compliance/app/sdk/iace/[projectId]/cra/_hooks/useCRA.ts b/admin-compliance/app/sdk/iace/[projectId]/cra/_hooks/useCRA.ts index 873ea101..237a2a36 100644 --- a/admin-compliance/app/sdk/iace/[projectId]/cra/_hooks/useCRA.ts +++ b/admin-compliance/app/sdk/iace/[projectId]/cra/_hooks/useCRA.ts @@ -2,6 +2,7 @@ import { useCallback, useEffect, useState } from 'react' import { CRADemo, CRAFinding, Measure, DEMO_SCENARIO } from './useCRADemo' +import { fetchProjectSafetyFunctions, SafetyFunction } from './iace-safety-bridge' // Live CRA assessment: POST the (demo) findings + customer weights + the project's // safety functions to /api/v1/cra/assess and merge the live, priority-sorted @@ -97,14 +98,26 @@ export function useCRA(projectId?: string) { const [scannerRepo, setScannerRepoState] = useState('') // pull-flow: chosen scanner repo_id const [weights, setWeights] = useState({}) const [snapshots, setSnapshots] = useState([]) + // Cyber-meets-CE: real safety functions derived from THIS IACE project's + // hazards/mitigations (falls back to the demo set until they load / if empty). + const [safetyFunctions, setSafetyFunctions] = useState(SAFETY_FUNCTIONS) + + useEffect(() => { + if (!projectId) return + let cancelled = false + fetchProjectSafetyFunctions(projectId).then((sf) => { + if (!cancelled && sf.length) setSafetyFunctions(sf) + }) + return () => { cancelled = true } + }, [projectId]) const buildPayload = useCallback(() => ({ findings: DEMO_SCENARIO.findings.map((f) => ({ id: f.id, title: f.title, cwe: f.cwe, severity: f.scanner_severity, location: f.location, })), weights, - safety_functions: SAFETY_FUNCTIONS, - }), [weights]) + safety_functions: safetyFunctions, + }), [weights, safetyFunctions]) useEffect(() => { let cancelled = false @@ -114,7 +127,7 @@ export function useCRA(projectId?: string) { try { const r = await fetch('/api/v1/cra/assess-from-scanner', { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ repo_id: scannerRepo, weights, safety_functions: SAFETY_FUNCTIONS }), + body: JSON.stringify({ repo_id: scannerRepo, weights, safety_functions: safetyFunctions }), }) if (r.ok) { const j = await r.json() @@ -151,7 +164,7 @@ export function useCRA(projectId?: string) { } })() return () => { cancelled = true } - }, [buildPayload, projectId, scannerRepo, weights]) + }, [buildPayload, projectId, scannerRepo, weights, safetyFunctions]) const refreshSnapshots = useCallback(() => { if (!projectId) return