feat(gap): IST-Zustand Assessment — IACE + Normen + Prozesse
Gap Analysis v2: statt 500 generische Gaps → nur die ECHTEN Lücken. Backend: - ProductProfile um 15 IST-Felder erweitert (Normen, Doku, Prozesse, CE) - assessGapStatus prüft: IACE-Mitigations → Zertifizierungen → Normen → IST-Felder - norm_mapping.go: 20 Normen → MC-Topic Mapping (ISO 12100, IEC 62443, etc.) - IACE-Integration: CheckIACECoverage() matcht verified Mitigations gegen MCs Frontend: - 2-Step Wizard: Produkt beschreiben → IST-Zustand erfassen - IstAssessment.tsx: CE-Jahr, Normen-Multiselect, Doku+Prozess Checkboxen - Step-Navigation mit visuellen Indikatoren Migration 025 erweitert um IST-Felder. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,166 @@
|
||||
'use client'
|
||||
|
||||
import React from 'react'
|
||||
|
||||
const NORMS = [
|
||||
{ value: 'ISO12100', label: 'ISO 12100 (Maschinensicherheit)' },
|
||||
{ value: 'ENISO13849', label: 'EN ISO 13849 (Sicherheitsfunktionen)' },
|
||||
{ value: 'IEC61508', label: 'IEC 61508 (Funktionale Sicherheit)' },
|
||||
{ value: 'IEC62443', label: 'IEC 62443 (Industrielle Cybersecurity)' },
|
||||
{ value: 'ISO27001', label: 'ISO 27001 (Informationssicherheit)' },
|
||||
{ value: 'ISO27002', label: 'ISO 27002 (Security Controls)' },
|
||||
{ value: 'EN61326', label: 'EN 61326 (EMV)' },
|
||||
{ value: 'EN62368', label: 'EN 62368 (Audio/Video/IT-Sicherheit)' },
|
||||
{ value: 'IEC60204', label: 'IEC 60204 (Elektrische Ausruestung)' },
|
||||
{ value: 'ISO13485', label: 'ISO 13485 (Medizinprodukte QM)' },
|
||||
{ value: 'ISO14971', label: 'ISO 14971 (Risikomanagement Medizin)' },
|
||||
{ value: 'IEC62304', label: 'IEC 62304 (Medizin-Software Lifecycle)' },
|
||||
{ value: 'ISO9001', label: 'ISO 9001 (Qualitaetsmanagement)' },
|
||||
{ value: 'ISO22301', label: 'ISO 22301 (Business Continuity)' },
|
||||
{ value: 'PCIDSS', label: 'PCI DSS (Zahlungssicherheit)' },
|
||||
{ value: 'EN50581', label: 'EN 50581 (RoHS/REACH)' },
|
||||
{ value: 'ASPICE', label: 'ASPICE (Automotive Software)' },
|
||||
]
|
||||
|
||||
interface IstData {
|
||||
applied_norms: string[]
|
||||
has_risk_assessment: boolean
|
||||
has_technical_file: boolean
|
||||
has_operating_manual: boolean
|
||||
has_sbom: boolean
|
||||
has_vuln_management: boolean
|
||||
has_update_mechanism: boolean
|
||||
has_incident_response: boolean
|
||||
has_supply_chain_mgmt: boolean
|
||||
ce_marking_since: string
|
||||
product_age: string
|
||||
}
|
||||
|
||||
interface Props {
|
||||
data: IstData
|
||||
onChange: (data: IstData) => void
|
||||
}
|
||||
|
||||
export function IstAssessment({ data, onChange }: Props) {
|
||||
const update = (field: string, value: unknown) => {
|
||||
onChange({ ...data, [field]: value })
|
||||
}
|
||||
|
||||
const toggleNorm = (norm: string) => {
|
||||
const norms = data.applied_norms.includes(norm)
|
||||
? data.applied_norms.filter(n => n !== norm)
|
||||
: [...data.applied_norms, norm]
|
||||
update('applied_norms', norms)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4">
|
||||
<p className="text-blue-800 text-sm">
|
||||
Geben Sie an was Sie bereits haben. Je mehr wir wissen, desto
|
||||
praeziser ist die Gap-Analyse. Controls die bereits erfuellt sind
|
||||
werden automatisch als "erledigt" markiert.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* CE-Kennzeichnung */}
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold text-gray-800 mb-3">CE-Kennzeichnung</h3>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-xs text-gray-500 mb-1">CE seit (Jahr)</label>
|
||||
<input
|
||||
type="text"
|
||||
value={data.ce_marking_since}
|
||||
onChange={e => update('ce_marking_since', e.target.value)}
|
||||
placeholder="z.B. 2016"
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs text-gray-500 mb-1">Produktalter</label>
|
||||
<select
|
||||
value={data.product_age}
|
||||
onChange={e => update('product_age', e.target.value)}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm"
|
||||
>
|
||||
<option value="">Bitte waehlen</option>
|
||||
<option value="new">Neues Produkt (noch nicht am Markt)</option>
|
||||
<option value="1_year">1 Jahr</option>
|
||||
<option value="3_years">2-3 Jahre</option>
|
||||
<option value="5_years">4-5 Jahre</option>
|
||||
<option value="10_years">6-10 Jahre</option>
|
||||
<option value="10_plus">Ueber 10 Jahre</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Angewandte Normen */}
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold text-gray-800 mb-3">Angewandte Normen</h3>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{NORMS.map(n => (
|
||||
<button
|
||||
key={n.value}
|
||||
onClick={() => toggleNorm(n.value)}
|
||||
className={`px-3 py-1.5 rounded-full text-xs border transition-colors ${
|
||||
data.applied_norms.includes(n.value)
|
||||
? 'bg-green-100 border-green-400 text-green-800'
|
||||
: 'border-gray-200 text-gray-600 hover:bg-gray-50'
|
||||
}`}
|
||||
>
|
||||
{n.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Bestehende Dokumentation */}
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold text-gray-800 mb-3">Bestehende Dokumentation</h3>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
{[
|
||||
{ field: 'has_risk_assessment', label: 'Risikobeurteilung vorhanden' },
|
||||
{ field: 'has_technical_file', label: 'Technische Dokumentation vorhanden' },
|
||||
{ field: 'has_operating_manual', label: 'Betriebsanleitung vorhanden' },
|
||||
{ field: 'has_sbom', label: 'SBOM (Software Bill of Materials)' },
|
||||
].map(item => (
|
||||
<label key={item.field} className="flex items-center gap-3 cursor-pointer p-2 rounded-lg hover:bg-gray-50">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={(data as Record<string, unknown>)[item.field] as boolean}
|
||||
onChange={e => update(item.field, e.target.checked)}
|
||||
className="w-4 h-4 rounded border-gray-300 text-green-600"
|
||||
/>
|
||||
<span className="text-sm text-gray-700">{item.label}</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Bestehende Prozesse */}
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold text-gray-800 mb-3">Bestehende Prozesse</h3>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
{[
|
||||
{ field: 'has_vuln_management', label: 'Schwachstellenmanagement' },
|
||||
{ field: 'has_update_mechanism', label: 'Software-Update-Mechanismus' },
|
||||
{ field: 'has_incident_response', label: 'Incident Response Prozess' },
|
||||
{ field: 'has_supply_chain_mgmt', label: 'Lieferketten-Management' },
|
||||
].map(item => (
|
||||
<label key={item.field} className="flex items-center gap-3 cursor-pointer p-2 rounded-lg hover:bg-gray-50">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={(data as Record<string, unknown>)[item.field] as boolean}
|
||||
onChange={e => update(item.field, e.target.checked)}
|
||||
className="w-4 h-4 rounded border-gray-300 text-green-600"
|
||||
/>
|
||||
<span className="text-sm text-gray-700">{item.label}</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
'use client'
|
||||
|
||||
import React, { useState } from 'react'
|
||||
import { IstAssessment } from './IstAssessment'
|
||||
|
||||
const PRODUCT_TYPES = [
|
||||
{ value: 'iot', label: 'IoT / Connected Device' },
|
||||
@@ -60,6 +61,20 @@ export function ProductWizard({ onAnalyze, loading }: Props) {
|
||||
const [usesAI, setUsesAI] = useState(false)
|
||||
const [processesPersonalData, setProcessesPersonalData] = useState(false)
|
||||
const [isCriticalInfra, setIsCriticalInfra] = useState(false)
|
||||
const [step, setStep] = useState(1)
|
||||
const [istData, setIstData] = useState({
|
||||
applied_norms: [] as string[],
|
||||
has_risk_assessment: false,
|
||||
has_technical_file: false,
|
||||
has_operating_manual: false,
|
||||
has_sbom: false,
|
||||
has_vuln_management: false,
|
||||
has_update_mechanism: false,
|
||||
has_incident_response: false,
|
||||
has_supply_chain_mgmt: false,
|
||||
ce_marking_since: '',
|
||||
product_age: '',
|
||||
})
|
||||
|
||||
const toggleArrayValue = (
|
||||
arr: string[],
|
||||
@@ -83,11 +98,59 @@ export function ProductWizard({ onAnalyze, loading }: Props) {
|
||||
processes_personal_data: processesPersonalData,
|
||||
is_critical_infra_supplier: isCriticalInfra,
|
||||
existing_certifications: certifications,
|
||||
...istData,
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="bg-white rounded-xl shadow-sm border border-gray-200 p-8">
|
||||
{/* Step Indicator */}
|
||||
<div className="flex items-center gap-4 mb-8">
|
||||
<button
|
||||
onClick={() => setStep(1)}
|
||||
className={`flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium ${
|
||||
step === 1 ? 'bg-blue-100 text-blue-700' : 'text-gray-500 hover:bg-gray-50'
|
||||
}`}
|
||||
>
|
||||
<span className="w-6 h-6 rounded-full bg-blue-600 text-white text-xs flex items-center justify-center">1</span>
|
||||
Produkt beschreiben
|
||||
</button>
|
||||
<span className="text-gray-300">→</span>
|
||||
<button
|
||||
onClick={() => productType ? setStep(2) : null}
|
||||
className={`flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium ${
|
||||
step === 2 ? 'bg-blue-100 text-blue-700' : 'text-gray-500 hover:bg-gray-50'
|
||||
} ${!productType ? 'opacity-50 cursor-not-allowed' : ''}`}
|
||||
>
|
||||
<span className={`w-6 h-6 rounded-full text-xs flex items-center justify-center ${
|
||||
step === 2 ? 'bg-blue-600 text-white' : 'bg-gray-300 text-white'
|
||||
}`}>2</span>
|
||||
IST-Zustand
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{step === 2 && (
|
||||
<>
|
||||
<IstAssessment data={istData} onChange={setIstData} />
|
||||
<div className="flex gap-4 mt-8">
|
||||
<button
|
||||
onClick={() => setStep(1)}
|
||||
className="px-6 py-3 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50"
|
||||
>
|
||||
Zurueck
|
||||
</button>
|
||||
<button
|
||||
onClick={handleSubmit}
|
||||
disabled={loading}
|
||||
className="flex-1 py-3 bg-blue-600 text-white font-medium rounded-lg hover:bg-blue-700 disabled:bg-gray-300 transition-colors"
|
||||
>
|
||||
{loading ? 'Analyse laeuft...' : 'Gap-Analyse starten'}
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{step === 1 && (<>
|
||||
{/* Produktname */}
|
||||
<div className="mb-6">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
@@ -225,14 +288,15 @@ export function ProductWizard({ onAnalyze, loading }: Props) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Submit */}
|
||||
{/* Next Step */}
|
||||
<button
|
||||
onClick={handleSubmit}
|
||||
disabled={!productType || loading}
|
||||
onClick={() => setStep(2)}
|
||||
disabled={!productType}
|
||||
className="w-full py-3 bg-blue-600 text-white font-medium rounded-lg hover:bg-blue-700 disabled:bg-gray-300 disabled:cursor-not-allowed transition-colors"
|
||||
>
|
||||
{loading ? 'Analyse laeuft...' : 'Gap-Analyse starten'}
|
||||
Weiter: IST-Zustand erfassen →
|
||||
</button>
|
||||
</>)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user