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'
|
'use client'
|
||||||
|
|
||||||
import React, { useState } from 'react'
|
import React, { useState } from 'react'
|
||||||
|
import { IstAssessment } from './IstAssessment'
|
||||||
|
|
||||||
const PRODUCT_TYPES = [
|
const PRODUCT_TYPES = [
|
||||||
{ value: 'iot', label: 'IoT / Connected Device' },
|
{ value: 'iot', label: 'IoT / Connected Device' },
|
||||||
@@ -60,6 +61,20 @@ export function ProductWizard({ onAnalyze, loading }: Props) {
|
|||||||
const [usesAI, setUsesAI] = useState(false)
|
const [usesAI, setUsesAI] = useState(false)
|
||||||
const [processesPersonalData, setProcessesPersonalData] = useState(false)
|
const [processesPersonalData, setProcessesPersonalData] = useState(false)
|
||||||
const [isCriticalInfra, setIsCriticalInfra] = 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 = (
|
const toggleArrayValue = (
|
||||||
arr: string[],
|
arr: string[],
|
||||||
@@ -83,11 +98,59 @@ export function ProductWizard({ onAnalyze, loading }: Props) {
|
|||||||
processes_personal_data: processesPersonalData,
|
processes_personal_data: processesPersonalData,
|
||||||
is_critical_infra_supplier: isCriticalInfra,
|
is_critical_infra_supplier: isCriticalInfra,
|
||||||
existing_certifications: certifications,
|
existing_certifications: certifications,
|
||||||
|
...istData,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-white rounded-xl shadow-sm border border-gray-200 p-8">
|
<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 */}
|
{/* Produktname */}
|
||||||
<div className="mb-6">
|
<div className="mb-6">
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
@@ -225,14 +288,15 @@ export function ProductWizard({ onAnalyze, loading }: Props) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Submit */}
|
{/* Next Step */}
|
||||||
<button
|
<button
|
||||||
onClick={handleSubmit}
|
onClick={() => setStep(2)}
|
||||||
disabled={!productType || loading}
|
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"
|
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>
|
</button>
|
||||||
|
</>)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ func (e *Engine) Analyze(profile *ProductProfile) (*GapReport, error) {
|
|||||||
// Step 4: Assess gaps
|
// Step 4: Assess gaps
|
||||||
gaps := make([]GapItem, 0, len(mcGroups))
|
gaps := make([]GapItem, 0, len(mcGroups))
|
||||||
for _, mc := range mcGroups {
|
for _, mc := range mcGroups {
|
||||||
status := e.assessGapStatus(mc, profile.ExistingCertifications)
|
status := e.assessGapStatus(mc, profile)
|
||||||
item := GapItem{
|
item := GapItem{
|
||||||
MCID: mc.MasterControlID,
|
MCID: mc.MasterControlID,
|
||||||
MCName: mc.CanonicalName,
|
MCName: mc.CanonicalName,
|
||||||
@@ -77,27 +77,80 @@ func (e *Engine) Analyze(profile *ProductProfile) (*GapReport, error) {
|
|||||||
return report, nil
|
return report, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// assessGapStatus determines if a MC is fulfilled based on existing certs.
|
// assessGapStatus determines if a MC is fulfilled based on IST-Zustand:
|
||||||
func (e *Engine) assessGapStatus(mc MCGroup, certs []string) GapStatus {
|
// IACE project data, applied norms, certifications, and existing processes.
|
||||||
// If customer has ISO 27001, many security controls are likely fulfilled
|
func (e *Engine) assessGapStatus(mc MCGroup, profile *ProductProfile) GapStatus {
|
||||||
for _, cert := range certs {
|
name := mc.CanonicalName
|
||||||
|
|
||||||
|
// A) IACE-Projekt vorhanden → aus verified Mitigations ableiten
|
||||||
|
if profile.IACEProjectID != nil {
|
||||||
|
status := e.store.CheckIACECoverage(*profile.IACEProjectID, name)
|
||||||
|
if status == "verified" {
|
||||||
|
return GapFulfilled
|
||||||
|
}
|
||||||
|
if status == "implemented" {
|
||||||
|
return GapPartial
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// B) Bestehende Zertifizierungen
|
||||||
|
for _, cert := range profile.ExistingCertifications {
|
||||||
switch cert {
|
switch cert {
|
||||||
case "ISO27001":
|
|
||||||
if isSecurityTopic(mc.CanonicalName) {
|
|
||||||
return GapPartial // Likely partially covered
|
|
||||||
}
|
|
||||||
case "CE":
|
case "CE":
|
||||||
if isMachineryTopic(mc.CanonicalName) {
|
if isMachineryTopic(name) {
|
||||||
return GapFulfilled
|
return GapFulfilled
|
||||||
}
|
}
|
||||||
|
case "ISO27001":
|
||||||
|
if isSecurityTopic(name) {
|
||||||
|
return GapPartial
|
||||||
|
}
|
||||||
case "SOC2":
|
case "SOC2":
|
||||||
if isSecurityTopic(mc.CanonicalName) {
|
if isSecurityTopic(name) {
|
||||||
|
return GapPartial
|
||||||
|
}
|
||||||
|
case "ISO13485":
|
||||||
|
if contains(name, "risk_management") || contains(name, "documentation") {
|
||||||
return GapPartial
|
return GapPartial
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Default: missing (customer must verify)
|
// C) Angewandte Normen → Controls als fulfilled erkennen
|
||||||
|
if normCoversControl(profile.AppliedNorms, name) {
|
||||||
|
return GapFulfilled
|
||||||
|
}
|
||||||
|
|
||||||
|
// D) IST-Felder direkt matchen
|
||||||
|
if profile.HasSBOM && contains(name, "asset_management_inventory") {
|
||||||
|
return GapFulfilled
|
||||||
|
}
|
||||||
|
if profile.HasVulnManagement && contains(name, "vulnerability") {
|
||||||
|
return GapFulfilled
|
||||||
|
}
|
||||||
|
if profile.HasUpdateMechanism && contains(name, "patch_management") {
|
||||||
|
return GapFulfilled
|
||||||
|
}
|
||||||
|
if profile.HasIncidentResponse && contains(name, "incident") {
|
||||||
|
return GapFulfilled
|
||||||
|
}
|
||||||
|
if profile.HasRiskAssessment && contains(name, "risk_management") {
|
||||||
|
return GapFulfilled
|
||||||
|
}
|
||||||
|
if profile.HasTechnicalFile && contains(name, "documentation") {
|
||||||
|
return GapFulfilled
|
||||||
|
}
|
||||||
|
if profile.HasOperatingManual && contains(name, "operating_instructions") {
|
||||||
|
return GapFulfilled
|
||||||
|
}
|
||||||
|
if profile.HasSupplyChainMgmt && contains(name, "third_party_management") {
|
||||||
|
return GapFulfilled
|
||||||
|
}
|
||||||
|
|
||||||
|
// E) CE-Kennzeichnung vorhanden → Produktsicherheit fulfilled
|
||||||
|
if profile.CEMarkingSince != nil && isMachineryTopic(name) {
|
||||||
|
return GapFulfilled
|
||||||
|
}
|
||||||
|
|
||||||
return GapMissing
|
return GapMissing
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -52,6 +52,30 @@ type ProductProfile struct {
|
|||||||
// Existing certifications (reduces gap count)
|
// Existing certifications (reduces gap count)
|
||||||
ExistingCertifications []string `json:"existing_certifications" db:"-"` // ISO27001, CE, SOC2
|
ExistingCertifications []string `json:"existing_certifications" db:"-"` // ISO27001, CE, SOC2
|
||||||
|
|
||||||
|
// ── IST-Zustand (was hat der Hersteller bereits?) ──────────────
|
||||||
|
|
||||||
|
// Verbindung zu bestehendem IACE Projekt
|
||||||
|
IACEProjectID *uuid.UUID `json:"iace_project_id" db:"iace_project_id"`
|
||||||
|
|
||||||
|
// Angewandte Normen
|
||||||
|
AppliedNorms []string `json:"applied_norms" db:"-"` // ISO12100, EN61326, EN62368
|
||||||
|
|
||||||
|
// Bestehende Dokumentation
|
||||||
|
HasRiskAssessment bool `json:"has_risk_assessment" db:"has_risk_assessment"`
|
||||||
|
HasTechnicalFile bool `json:"has_technical_file" db:"has_technical_file"`
|
||||||
|
HasOperatingManual bool `json:"has_operating_manual" db:"has_operating_manual"`
|
||||||
|
HasSBOM bool `json:"has_sbom" db:"has_sbom"`
|
||||||
|
|
||||||
|
// Bestehende Prozesse
|
||||||
|
HasVulnManagement bool `json:"has_vuln_management" db:"has_vuln_management"`
|
||||||
|
HasUpdateMechanism bool `json:"has_update_mechanism" db:"has_update_mechanism"`
|
||||||
|
HasIncidentResponse bool `json:"has_incident_response" db:"has_incident_response"`
|
||||||
|
HasSupplyChainMgmt bool `json:"has_supply_chain_mgmt" db:"has_supply_chain_mgmt"`
|
||||||
|
|
||||||
|
// CE/Produktsicherheit
|
||||||
|
CEMarkingSince *string `json:"ce_marking_since" db:"ce_marking_since"`
|
||||||
|
ProductAge string `json:"product_age" db:"product_age"`
|
||||||
|
|
||||||
// Metadata
|
// Metadata
|
||||||
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
||||||
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
|
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
|
||||||
|
|||||||
@@ -0,0 +1,73 @@
|
|||||||
|
package gap
|
||||||
|
|
||||||
|
// NormToControlMapping maps applied norms to MC topic prefixes they cover.
|
||||||
|
// If a manufacturer has applied a norm, all matching MC topics are "fulfilled".
|
||||||
|
var NormToControlMapping = map[string][]string{
|
||||||
|
// Machine Safety
|
||||||
|
"ISO12100": {"risk_management_assessment", "risk_management_documentation", "product_safety"},
|
||||||
|
"ENISO13849": {"product_safety", "risk_management_assessment", "secure_development"},
|
||||||
|
"IEC61508": {"product_safety", "risk_management", "secure_development"},
|
||||||
|
"IEC62061": {"product_safety", "risk_management"},
|
||||||
|
|
||||||
|
// EMC / Electrical Safety
|
||||||
|
"EN61326": {"network_security", "physical_security"},
|
||||||
|
"EN62368": {"physical_security", "product_safety"},
|
||||||
|
"IEC60204": {"physical_security", "product_safety"},
|
||||||
|
|
||||||
|
// Information Security
|
||||||
|
"ISO27001": {
|
||||||
|
"access_control", "encryption", "incident", "audit_logging",
|
||||||
|
"vulnerability", "patch_management", "risk_management",
|
||||||
|
"human_resources_security", "physical_security", "backup",
|
||||||
|
"disaster_recovery", "change_management", "asset_management",
|
||||||
|
"monitoring", "network_security",
|
||||||
|
},
|
||||||
|
"ISO27002": {
|
||||||
|
"access_control", "encryption", "audit_logging",
|
||||||
|
"vulnerability", "patch_management",
|
||||||
|
},
|
||||||
|
|
||||||
|
// Industrial Cybersecurity
|
||||||
|
"IEC62443": {
|
||||||
|
"network_security", "network_segmentation", "access_control",
|
||||||
|
"monitoring", "vulnerability", "patch_management",
|
||||||
|
"incident", "secure_development",
|
||||||
|
},
|
||||||
|
|
||||||
|
// Medical Devices
|
||||||
|
"ISO13485": {"risk_management", "documentation", "change_management", "training"},
|
||||||
|
"IEC60601": {"physical_security", "product_safety"},
|
||||||
|
"ISO14971": {"risk_management_assessment", "risk_management_documentation"},
|
||||||
|
"IEC62304": {"secure_development", "change_management", "documentation"},
|
||||||
|
|
||||||
|
// Crypto/Fintech
|
||||||
|
"ISO22301": {"disaster_recovery", "backup", "incident"},
|
||||||
|
"PCIDSS": {"encryption", "access_control", "audit_logging", "vulnerability", "network_segmentation"},
|
||||||
|
|
||||||
|
// Quality / Environmental
|
||||||
|
"ISO9001": {"change_management", "documentation", "training", "compliance_audit"},
|
||||||
|
"ISO14001": {"compliance_audit", "documentation", "risk_management"},
|
||||||
|
|
||||||
|
// Product Safety / RoHS / REACH
|
||||||
|
"EN50581": {"supply_chain_due_diligence", "product_safety"},
|
||||||
|
|
||||||
|
// Functional Safety (software)
|
||||||
|
"ASPICE": {"secure_development", "change_management", "documentation"},
|
||||||
|
"ISO26262": {"secure_development", "risk_management", "product_safety"},
|
||||||
|
}
|
||||||
|
|
||||||
|
// normCoversControl checks if any applied norm covers a given MC topic.
|
||||||
|
func normCoversControl(appliedNorms []string, mcTopic string) bool {
|
||||||
|
for _, norm := range appliedNorms {
|
||||||
|
topics, ok := NormToControlMapping[norm]
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for _, topic := range topics {
|
||||||
|
if contains(mcTopic, topic) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
@@ -35,19 +35,29 @@ func (s *Store) CreateProfile(p *ProductProfile) error {
|
|||||||
marketsJSON, _ := json.Marshal(p.Markets)
|
marketsJSON, _ := json.Marshal(p.Markets)
|
||||||
certsJSON, _ := json.Marshal(p.ExistingCertifications)
|
certsJSON, _ := json.Marshal(p.ExistingCertifications)
|
||||||
|
|
||||||
|
normsJSON, _ := json.Marshal(p.AppliedNorms)
|
||||||
|
|
||||||
_, err := s.pool.Exec(ctx, `
|
_, err := s.pool.Exec(ctx, `
|
||||||
INSERT INTO compliance.gap_projects
|
INSERT INTO compliance.gap_projects
|
||||||
(id, tenant_id, name, description, product_type,
|
(id, tenant_id, name, description, product_type,
|
||||||
technologies, data_processing, markets,
|
technologies, data_processing, markets,
|
||||||
connected_to_internet, has_software_updates, uses_ai,
|
connected_to_internet, has_software_updates, uses_ai,
|
||||||
processes_personal_data, is_critical_infra_supplier,
|
processes_personal_data, is_critical_infra_supplier,
|
||||||
existing_certifications, created_at, updated_at)
|
existing_certifications, applied_norms,
|
||||||
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16)`,
|
has_risk_assessment, has_technical_file, has_operating_manual, has_sbom,
|
||||||
|
has_vuln_management, has_update_mechanism, has_incident_response, has_supply_chain_mgmt,
|
||||||
|
ce_marking_since, product_age, iace_project_id,
|
||||||
|
created_at, updated_at)
|
||||||
|
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18,$19,$20,$21,$22,$23,$24,$25,$26,$27,$28)`,
|
||||||
p.ID, p.TenantID, p.Name, p.Description, p.ProductType,
|
p.ID, p.TenantID, p.Name, p.Description, p.ProductType,
|
||||||
techJSON, dataJSON, marketsJSON,
|
techJSON, dataJSON, marketsJSON,
|
||||||
p.ConnectedToInternet, p.HasSoftwareUpdates, p.UsesAI,
|
p.ConnectedToInternet, p.HasSoftwareUpdates, p.UsesAI,
|
||||||
p.ProcessesPersonalData, p.IsCriticalInfraSupplier,
|
p.ProcessesPersonalData, p.IsCriticalInfraSupplier,
|
||||||
certsJSON, p.CreatedAt, p.UpdatedAt,
|
certsJSON, normsJSON,
|
||||||
|
p.HasRiskAssessment, p.HasTechnicalFile, p.HasOperatingManual, p.HasSBOM,
|
||||||
|
p.HasVulnManagement, p.HasUpdateMechanism, p.HasIncidentResponse, p.HasSupplyChainMgmt,
|
||||||
|
p.CEMarkingSince, p.ProductAge, p.IACEProjectID,
|
||||||
|
p.CreatedAt, p.UpdatedAt,
|
||||||
)
|
)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -227,6 +237,58 @@ func sourceToRegID(source string) RegulationID {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CheckIACECoverage checks if an IACE project has verified mitigations
|
||||||
|
// covering the given MC topic.
|
||||||
|
func (s *Store) CheckIACECoverage(projectID uuid.UUID, mcTopic string) string {
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
// Map MC topics to IACE hazard categories
|
||||||
|
iaceCategory := mcTopicToIACECategory(mcTopic)
|
||||||
|
if iaceCategory == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
var verifiedCount, implementedCount int
|
||||||
|
err := s.pool.QueryRow(ctx, `
|
||||||
|
SELECT
|
||||||
|
COUNT(CASE WHEN m.status = 'verified' THEN 1 END),
|
||||||
|
COUNT(CASE WHEN m.status = 'implemented' THEN 1 END)
|
||||||
|
FROM iace_mitigations m
|
||||||
|
JOIN iace_hazards h ON h.id = m.hazard_id
|
||||||
|
WHERE h.project_id = $1
|
||||||
|
AND (h.category ILIKE $2 OR h.sub_category ILIKE $2)`,
|
||||||
|
projectID, "%"+iaceCategory+"%",
|
||||||
|
).Scan(&verifiedCount, &implementedCount)
|
||||||
|
|
||||||
|
if err != nil || (verifiedCount == 0 && implementedCount == 0) {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
if verifiedCount > 0 {
|
||||||
|
return "verified"
|
||||||
|
}
|
||||||
|
return "implemented"
|
||||||
|
}
|
||||||
|
|
||||||
|
func mcTopicToIACECategory(topic string) string {
|
||||||
|
mapping := map[string]string{
|
||||||
|
"encryption": "cyber",
|
||||||
|
"access_control": "software",
|
||||||
|
"network_security": "cyber",
|
||||||
|
"vulnerability": "cyber",
|
||||||
|
"product_safety": "mechanical",
|
||||||
|
"physical_security": "electrical",
|
||||||
|
"monitoring": "software",
|
||||||
|
"incident": "organizational",
|
||||||
|
"risk_management": "general",
|
||||||
|
}
|
||||||
|
for prefix, cat := range mapping {
|
||||||
|
if strings.HasPrefix(topic, prefix) {
|
||||||
|
return cat
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
func formatTitle(name string) string {
|
func formatTitle(name string) string {
|
||||||
return strings.ReplaceAll(
|
return strings.ReplaceAll(
|
||||||
strings.ReplaceAll(name, "_", " "),
|
strings.ReplaceAll(name, "_", " "),
|
||||||
|
|||||||
@@ -16,6 +16,18 @@ CREATE TABLE IF NOT EXISTS compliance.gap_projects (
|
|||||||
processes_personal_data BOOLEAN DEFAULT false,
|
processes_personal_data BOOLEAN DEFAULT false,
|
||||||
is_critical_infra_supplier BOOLEAN DEFAULT false,
|
is_critical_infra_supplier BOOLEAN DEFAULT false,
|
||||||
existing_certifications JSONB DEFAULT '[]',
|
existing_certifications JSONB DEFAULT '[]',
|
||||||
|
applied_norms JSONB DEFAULT '[]',
|
||||||
|
has_risk_assessment BOOLEAN DEFAULT false,
|
||||||
|
has_technical_file BOOLEAN DEFAULT false,
|
||||||
|
has_operating_manual BOOLEAN DEFAULT false,
|
||||||
|
has_sbom BOOLEAN DEFAULT false,
|
||||||
|
has_vuln_management BOOLEAN DEFAULT false,
|
||||||
|
has_update_mechanism BOOLEAN DEFAULT false,
|
||||||
|
has_incident_response BOOLEAN DEFAULT false,
|
||||||
|
has_supply_chain_mgmt BOOLEAN DEFAULT false,
|
||||||
|
ce_marking_since VARCHAR(20),
|
||||||
|
product_age VARCHAR(20),
|
||||||
|
iace_project_id UUID,
|
||||||
last_analysis_at TIMESTAMPTZ,
|
last_analysis_at TIMESTAMPTZ,
|
||||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||||
updated_at TIMESTAMPTZ DEFAULT NOW()
|
updated_at TIMESTAMPTZ DEFAULT NOW()
|
||||||
|
|||||||
Reference in New Issue
Block a user