diff --git a/ai-compliance-sdk/internal/api/handlers/ucca_handlers.go b/ai-compliance-sdk/internal/api/handlers/ucca_handlers.go
index 97acb26..7ee1e77 100644
--- a/ai-compliance-sdk/internal/api/handlers/ucca_handlers.go
+++ b/ai-compliance-sdk/internal/api/handlers/ucca_handlers.go
@@ -1,13 +1,11 @@
package handlers
import (
- "bytes"
"crypto/sha256"
"encoding/hex"
"fmt"
"net/http"
"strconv"
- "strings"
"time"
"github.com/breakpilot/ai-compliance-sdk/internal/llm"
@@ -48,9 +46,13 @@ func NewUCCAHandlers(store *ucca.Store, escalationStore *ucca.EscalationStore, p
}
}
-// ============================================================================
-// POST /sdk/v1/ucca/assess - Evaluate a use case
-// ============================================================================
+// evaluateIntake runs evaluation using YAML engine or legacy fallback
+func (h *UCCAHandlers) evaluateIntake(intake *ucca.UseCaseIntake) (*ucca.AssessmentResult, string) {
+ if h.policyEngine != nil {
+ return h.policyEngine.Evaluate(intake), h.policyEngine.GetPolicyVersion()
+ }
+ return h.legacyRuleEngine.Evaluate(intake), "1.0.0-legacy"
+}
// Assess evaluates a use case intake and creates an assessment
func (h *UCCAHandlers) Assess(c *gin.Context) {
@@ -67,22 +69,12 @@ func (h *UCCAHandlers) Assess(c *gin.Context) {
return
}
- // Run evaluation - prefer YAML-based policy engine if available
- var result *ucca.AssessmentResult
- var policyVersion string
- if h.policyEngine != nil {
- result = h.policyEngine.Evaluate(&intake)
- policyVersion = h.policyEngine.GetPolicyVersion()
- } else {
- result = h.legacyRuleEngine.Evaluate(&intake)
- policyVersion = "1.0.0-legacy"
- }
+ result, policyVersion := h.evaluateIntake(&intake)
// Calculate hash of use case text
hash := sha256.Sum256([]byte(intake.UseCaseText))
hashStr := hex.EncodeToString(hash[:])
- // Create assessment record
assessment := &ucca.Assessment{
TenantID: tenantID,
Title: intake.Title,
@@ -107,89 +99,19 @@ func (h *UCCAHandlers) Assess(c *gin.Context) {
CreatedBy: userID,
}
- // Clear use case text if not opted in to store
if !intake.StoreRawText {
assessment.Intake.UseCaseText = ""
}
-
- // Generate title if not provided
if assessment.Title == "" {
assessment.Title = fmt.Sprintf("Assessment vom %s", time.Now().Format("02.01.2006 15:04"))
}
- // Save to database
if err := h.store.CreateAssessment(c.Request.Context(), assessment); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
- // Automatically create escalation based on assessment result
- var escalation *ucca.Escalation
- if h.escalationStore != nil && h.escalationTrigger != nil {
- level, reason := h.escalationTrigger.DetermineEscalationLevel(result)
-
- // Calculate due date based on SLA
- responseHours, _ := ucca.GetDefaultSLA(level)
- var dueDate *time.Time
- if responseHours > 0 {
- due := time.Now().UTC().Add(time.Duration(responseHours) * time.Hour)
- dueDate = &due
- }
-
- escalation = &ucca.Escalation{
- TenantID: tenantID,
- AssessmentID: assessment.ID,
- EscalationLevel: level,
- EscalationReason: reason,
- Status: ucca.EscalationStatusPending,
- DueDate: dueDate,
- }
-
- // For E0, auto-approve
- if level == ucca.EscalationLevelE0 {
- escalation.Status = ucca.EscalationStatusApproved
- approveDecision := ucca.EscalationDecisionApprove
- escalation.Decision = &approveDecision
- now := time.Now().UTC()
- escalation.DecisionAt = &now
- autoNotes := "Automatische Freigabe (E0)"
- escalation.DecisionNotes = &autoNotes
- }
-
- if err := h.escalationStore.CreateEscalation(c.Request.Context(), escalation); err != nil {
- // Log error but don't fail the assessment creation
- fmt.Printf("Warning: Could not create escalation: %v\n", err)
- escalation = nil
- } else {
- // Add history entry
- h.escalationStore.AddEscalationHistory(c.Request.Context(), &ucca.EscalationHistory{
- EscalationID: escalation.ID,
- Action: "auto_created",
- NewStatus: string(escalation.Status),
- NewLevel: string(escalation.EscalationLevel),
- ActorID: userID,
- Notes: "Automatisch erstellt bei Assessment",
- })
-
- // For E1/E2/E3, try to auto-assign
- if level != ucca.EscalationLevelE0 {
- role := ucca.GetRoleForLevel(level)
- reviewer, err := h.escalationStore.GetNextAvailableReviewer(c.Request.Context(), tenantID, role)
- if err == nil && reviewer != nil {
- h.escalationStore.AssignEscalation(c.Request.Context(), escalation.ID, reviewer.UserID, role)
- h.escalationStore.IncrementReviewerCount(c.Request.Context(), reviewer.UserID)
- h.escalationStore.AddEscalationHistory(c.Request.Context(), &ucca.EscalationHistory{
- EscalationID: escalation.ID,
- Action: "auto_assigned",
- OldStatus: string(ucca.EscalationStatusPending),
- NewStatus: string(ucca.EscalationStatusAssigned),
- ActorID: userID,
- Notes: "Automatisch zugewiesen an: " + reviewer.UserName,
- })
- }
- }
- }
- }
+ escalation := h.createEscalationForAssessment(c, assessment, result, tenantID, userID)
c.JSON(http.StatusCreated, ucca.AssessResponse{
Assessment: *assessment,
@@ -198,10 +120,6 @@ func (h *UCCAHandlers) Assess(c *gin.Context) {
})
}
-// ============================================================================
-// GET /sdk/v1/ucca/assessments - List all assessments
-// ============================================================================
-
// ListAssessments returns all assessments for a tenant
func (h *UCCAHandlers) ListAssessments(c *gin.Context) {
tenantID := rbac.GetTenantID(c)
@@ -232,10 +150,6 @@ func (h *UCCAHandlers) ListAssessments(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"assessments": assessments, "total": total})
}
-// ============================================================================
-// GET /sdk/v1/ucca/assessments/:id - Get single assessment
-// ============================================================================
-
// GetAssessment returns a single assessment by ID
func (h *UCCAHandlers) GetAssessment(c *gin.Context) {
id, err := uuid.Parse(c.Param("id"))
@@ -257,10 +171,6 @@ func (h *UCCAHandlers) GetAssessment(c *gin.Context) {
c.JSON(http.StatusOK, assessment)
}
-// ============================================================================
-// DELETE /sdk/v1/ucca/assessments/:id - Delete assessment
-// ============================================================================
-
// DeleteAssessment deletes an assessment
func (h *UCCAHandlers) DeleteAssessment(c *gin.Context) {
id, err := uuid.Parse(c.Param("id"))
@@ -277,10 +187,6 @@ func (h *UCCAHandlers) DeleteAssessment(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"message": "deleted"})
}
-// ============================================================================
-// PUT /sdk/v1/ucca/assessments/:id - Update an existing assessment
-// ============================================================================
-
// UpdateAssessment re-evaluates and updates an existing assessment
func (h *UCCAHandlers) UpdateAssessment(c *gin.Context) {
id, err := uuid.Parse(c.Param("id"))
@@ -295,16 +201,7 @@ func (h *UCCAHandlers) UpdateAssessment(c *gin.Context) {
return
}
- // Re-run evaluation with updated intake
- var result *ucca.AssessmentResult
- var policyVersion string
- if h.policyEngine != nil {
- result = h.policyEngine.Evaluate(&intake)
- policyVersion = h.policyEngine.GetPolicyVersion()
- } else {
- result = h.legacyRuleEngine.Evaluate(&intake)
- policyVersion = "1.0.0-legacy"
- }
+ result, policyVersion := h.evaluateIntake(&intake)
hash := sha256.Sum256([]byte(intake.UseCaseText))
hashStr := hex.EncodeToString(hash[:])
@@ -349,470 +246,6 @@ func (h *UCCAHandlers) UpdateAssessment(c *gin.Context) {
c.JSON(http.StatusOK, assessment)
}
-// ============================================================================
-// GET /sdk/v1/ucca/patterns - Get pattern catalog
-// ============================================================================
-
-// ListPatterns returns all available architecture patterns
-func (h *UCCAHandlers) ListPatterns(c *gin.Context) {
- var response []gin.H
-
- // Prefer YAML-based patterns if available
- if h.policyEngine != nil {
- yamlPatterns := h.policyEngine.GetAllPatterns()
- response = make([]gin.H, 0, len(yamlPatterns))
- for _, p := range yamlPatterns {
- response = append(response, gin.H{
- "id": p.ID,
- "title": p.Title,
- "description": p.Description,
- "benefit": p.Benefit,
- "effort": p.Effort,
- "risk_reduction": p.RiskReduction,
- })
- }
- } else {
- // Fall back to legacy patterns
- patterns := ucca.GetAllPatterns()
- response = make([]gin.H, len(patterns))
- for i, p := range patterns {
- response[i] = gin.H{
- "id": p.ID,
- "title": p.Title,
- "title_de": p.TitleDE,
- "description": p.Description,
- "description_de": p.DescriptionDE,
- "benefits": p.Benefits,
- "requirements": p.Requirements,
- }
- }
- }
-
- c.JSON(http.StatusOK, gin.H{"patterns": response})
-}
-
-// ============================================================================
-// GET /sdk/v1/ucca/controls - Get control catalog
-// ============================================================================
-
-// ListControls returns all available compliance controls
-func (h *UCCAHandlers) ListControls(c *gin.Context) {
- var response []gin.H
-
- // Prefer YAML-based controls if available
- if h.policyEngine != nil {
- yamlControls := h.policyEngine.GetAllControls()
- response = make([]gin.H, 0, len(yamlControls))
- for _, ctrl := range yamlControls {
- response = append(response, gin.H{
- "id": ctrl.ID,
- "title": ctrl.Title,
- "description": ctrl.Description,
- "gdpr_ref": ctrl.GDPRRef,
- "effort": ctrl.Effort,
- })
- }
- } else {
- // Fall back to legacy controls
- for id, ctrl := range ucca.ControlLibrary {
- response = append(response, gin.H{
- "id": id,
- "title": ctrl.Title,
- "description": ctrl.Description,
- "severity": ctrl.Severity,
- "category": ctrl.Category,
- "gdpr_ref": ctrl.GDPRRef,
- })
- }
- }
-
- c.JSON(http.StatusOK, gin.H{"controls": response})
-}
-
-// ============================================================================
-// GET /sdk/v1/ucca/problem-solutions - Get problem-solution mappings
-// ============================================================================
-
-// ListProblemSolutions returns all problem-solution mappings
-func (h *UCCAHandlers) ListProblemSolutions(c *gin.Context) {
- if h.policyEngine == nil {
- c.JSON(http.StatusOK, gin.H{
- "problem_solutions": []gin.H{},
- "message": "Problem-solutions only available with YAML policy engine",
- })
- return
- }
-
- problemSolutions := h.policyEngine.GetProblemSolutions()
- response := make([]gin.H, len(problemSolutions))
- for i, ps := range problemSolutions {
- solutions := make([]gin.H, len(ps.Solutions))
- for j, sol := range ps.Solutions {
- solutions[j] = gin.H{
- "id": sol.ID,
- "title": sol.Title,
- "pattern": sol.Pattern,
- "control": sol.Control,
- "removes_problem": sol.RemovesProblem,
- "team_question": sol.TeamQuestion,
- }
- }
-
- triggers := make([]gin.H, len(ps.Triggers))
- for j, t := range ps.Triggers {
- triggers[j] = gin.H{
- "rule": t.Rule,
- "without_control": t.WithoutControl,
- }
- }
-
- response[i] = gin.H{
- "problem_id": ps.ProblemID,
- "title": ps.Title,
- "triggers": triggers,
- "solutions": solutions,
- }
- }
-
- c.JSON(http.StatusOK, gin.H{"problem_solutions": response})
-}
-
-// ============================================================================
-// GET /sdk/v1/ucca/examples - Get example catalog
-// ============================================================================
-
-// ListExamples returns all available didactic examples
-func (h *UCCAHandlers) ListExamples(c *gin.Context) {
- examples := ucca.GetAllExamples()
-
- // Convert to API response format
- response := make([]gin.H, len(examples))
- for i, ex := range examples {
- response[i] = gin.H{
- "id": ex.ID,
- "title": ex.Title,
- "title_de": ex.TitleDE,
- "description": ex.Description,
- "description_de": ex.DescriptionDE,
- "domain": ex.Domain,
- "outcome": ex.Outcome,
- "outcome_de": ex.OutcomeDE,
- "lessons": ex.Lessons,
- "lessons_de": ex.LessonsDE,
- }
- }
-
- c.JSON(http.StatusOK, gin.H{"examples": response})
-}
-
-// ============================================================================
-// GET /sdk/v1/ucca/rules - Get all rules (transparency)
-// ============================================================================
-
-// ListRules returns all rules for transparency
-func (h *UCCAHandlers) ListRules(c *gin.Context) {
- var response []gin.H
- var policyVersion string
-
- // Prefer YAML-based rules if available
- if h.policyEngine != nil {
- yamlRules := h.policyEngine.GetAllRules()
- policyVersion = h.policyEngine.GetPolicyVersion()
- response = make([]gin.H, len(yamlRules))
- for i, r := range yamlRules {
- response[i] = gin.H{
- "code": r.ID,
- "category": r.Category,
- "title": r.Title,
- "description": r.Description,
- "severity": r.Severity,
- "gdpr_ref": r.GDPRRef,
- "rationale": r.Rationale,
- "controls": r.Effect.ControlsAdd,
- "patterns": r.Effect.SuggestedPatterns,
- "risk_add": r.Effect.RiskAdd,
- }
- }
- } else {
- // Fall back to legacy rules
- rules := h.legacyRuleEngine.GetRules()
- policyVersion = "1.0.0-legacy"
- response = make([]gin.H, len(rules))
- for i, r := range rules {
- response[i] = gin.H{
- "code": r.Code,
- "category": r.Category,
- "title": r.Title,
- "title_de": r.TitleDE,
- "description": r.Description,
- "description_de": r.DescriptionDE,
- "severity": r.Severity,
- "score_delta": r.ScoreDelta,
- "gdpr_ref": r.GDPRRef,
- "controls": r.Controls,
- "patterns": r.Patterns,
- }
- }
- }
-
- c.JSON(http.StatusOK, gin.H{
- "rules": response,
- "total": len(response),
- "policy_version": policyVersion,
- "categories": []string{
- "A. Datenklassifikation",
- "B. Zweck & Rechtsgrundlage",
- "C. Automatisierung",
- "D. Training & Modell",
- "E. Hosting",
- "F. Domain-spezifisch",
- "G. Aggregation",
- },
- })
-}
-
-// ============================================================================
-// POST /sdk/v1/ucca/assessments/:id/explain - Generate LLM explanation
-// ============================================================================
-
-// Explain generates an LLM explanation for an assessment
-func (h *UCCAHandlers) Explain(c *gin.Context) {
- id, err := uuid.Parse(c.Param("id"))
- if err != nil {
- c.JSON(http.StatusBadRequest, gin.H{"error": "invalid ID"})
- return
- }
-
- var req ucca.ExplainRequest
- if err := c.ShouldBindJSON(&req); err != nil {
- // Default to German
- req.Language = "de"
- }
- if req.Language == "" {
- req.Language = "de"
- }
-
- // Get assessment
- assessment, err := h.store.GetAssessment(c.Request.Context(), id)
- if err != nil {
- c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
- return
- }
- if assessment == nil {
- c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
- return
- }
-
- // Get legal context from RAG
- var legalContext *ucca.LegalContext
- var legalContextStr string
- if h.legalRAGClient != nil {
- legalContext, err = h.legalRAGClient.GetLegalContextForAssessment(c.Request.Context(), assessment)
- if err != nil {
- // Log error but continue without legal context
- fmt.Printf("Warning: Could not get legal context: %v\n", err)
- } else {
- legalContextStr = h.legalRAGClient.FormatLegalContextForPrompt(legalContext)
- }
- }
-
- // Build prompt for LLM with legal context
- prompt := buildExplanationPrompt(assessment, req.Language, legalContextStr)
-
- // Call LLM
- chatReq := &llm.ChatRequest{
- Messages: []llm.Message{
- {Role: "system", Content: "Du bist ein Datenschutz-Experte, der DSGVO-Compliance-Bewertungen erklärt. Antworte klar, präzise und auf Deutsch. Beziehe dich auf die angegebenen Rechtsgrundlagen."},
- {Role: "user", Content: prompt},
- },
- MaxTokens: 2000,
- Temperature: 0.3,
- }
- response, err := h.providerRegistry.Chat(c.Request.Context(), chatReq)
- if err != nil {
- c.JSON(http.StatusInternalServerError, gin.H{"error": "LLM call failed: " + err.Error()})
- return
- }
-
- explanation := response.Message.Content
- model := response.Model
-
- // Save explanation to database
- if err := h.store.UpdateExplanation(c.Request.Context(), id, explanation, model); err != nil {
- c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
- return
- }
-
- c.JSON(http.StatusOK, ucca.ExplainResponse{
- ExplanationText: explanation,
- GeneratedAt: time.Now().UTC(),
- Model: model,
- LegalContext: legalContext,
- })
-}
-
-// buildExplanationPrompt creates the prompt for the LLM explanation
-func buildExplanationPrompt(assessment *ucca.Assessment, language string, legalContext string) string {
- var buf bytes.Buffer
-
- buf.WriteString("Erkläre die folgende DSGVO-Compliance-Bewertung für einen KI-Use-Case in verständlicher Sprache:\n\n")
-
- buf.WriteString(fmt.Sprintf("**Ergebnis:** %s\n", assessment.Feasibility))
- buf.WriteString(fmt.Sprintf("**Risikostufe:** %s\n", assessment.RiskLevel))
- buf.WriteString(fmt.Sprintf("**Risiko-Score:** %d/100\n", assessment.RiskScore))
- buf.WriteString(fmt.Sprintf("**Komplexität:** %s\n\n", assessment.Complexity))
-
- if len(assessment.TriggeredRules) > 0 {
- buf.WriteString("**Ausgelöste Regeln:**\n")
- for _, r := range assessment.TriggeredRules {
- buf.WriteString(fmt.Sprintf("- %s (%s): %s\n", r.Code, r.Severity, r.Title))
- }
- buf.WriteString("\n")
- }
-
- if len(assessment.RequiredControls) > 0 {
- buf.WriteString("**Erforderliche Maßnahmen:**\n")
- for _, c := range assessment.RequiredControls {
- buf.WriteString(fmt.Sprintf("- %s: %s\n", c.Title, c.Description))
- }
- buf.WriteString("\n")
- }
-
- if assessment.DSFARecommended {
- buf.WriteString("**Hinweis:** Eine Datenschutz-Folgenabschätzung (DSFA) wird empfohlen.\n\n")
- }
-
- if assessment.Art22Risk {
- buf.WriteString("**Warnung:** Es besteht ein Risiko unter Art. 22 DSGVO (automatisierte Einzelentscheidungen).\n\n")
- }
-
- // Include legal context from RAG if available
- if legalContext != "" {
- buf.WriteString(legalContext)
- }
-
- buf.WriteString("\nBitte erkläre:\n")
- buf.WriteString("1. Warum dieses Ergebnis zustande kam (mit Bezug auf die angegebenen Rechtsgrundlagen)\n")
- buf.WriteString("2. Welche konkreten Schritte unternommen werden sollten\n")
- buf.WriteString("3. Welche Alternativen es gibt, falls der Use Case abgelehnt wurde\n")
- buf.WriteString("4. Welche spezifischen Artikel aus DSGVO/AI Act beachtet werden müssen\n")
-
- return buf.String()
-}
-
-// ============================================================================
-// GET /sdk/v1/ucca/export/:id - Export assessment
-// ============================================================================
-
-// Export exports an assessment as JSON or Markdown
-func (h *UCCAHandlers) Export(c *gin.Context) {
- id, err := uuid.Parse(c.Param("id"))
- if err != nil {
- c.JSON(http.StatusBadRequest, gin.H{"error": "invalid ID"})
- return
- }
-
- format := c.DefaultQuery("format", "json")
-
- assessment, err := h.store.GetAssessment(c.Request.Context(), id)
- if err != nil {
- c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
- return
- }
- if assessment == nil {
- c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
- return
- }
-
- if format == "md" {
- markdown := generateMarkdownExport(assessment)
- c.Header("Content-Type", "text/markdown; charset=utf-8")
- c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=ucca_assessment_%s.md", id.String()[:8]))
- c.Data(http.StatusOK, "text/markdown; charset=utf-8", []byte(markdown))
- return
- }
-
- // JSON export
- c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=ucca_assessment_%s.json", id.String()[:8]))
- c.JSON(http.StatusOK, gin.H{
- "exported_at": time.Now().UTC().Format(time.RFC3339),
- "assessment": assessment,
- })
-}
-
-// generateMarkdownExport creates a Markdown export of the assessment
-func generateMarkdownExport(a *ucca.Assessment) string {
- var buf bytes.Buffer
-
- buf.WriteString("# UCCA Use-Case Assessment\n\n")
- buf.WriteString(fmt.Sprintf("**ID:** %s\n", a.ID.String()))
- buf.WriteString(fmt.Sprintf("**Erstellt:** %s\n", a.CreatedAt.Format("02.01.2006 15:04")))
- buf.WriteString(fmt.Sprintf("**Domain:** %s\n\n", a.Domain))
-
- buf.WriteString("## Ergebnis\n\n")
- buf.WriteString(fmt.Sprintf("| Kriterium | Wert |\n"))
- buf.WriteString("|-----------|------|\n")
- buf.WriteString(fmt.Sprintf("| Machbarkeit | **%s** |\n", a.Feasibility))
- buf.WriteString(fmt.Sprintf("| Risikostufe | %s |\n", a.RiskLevel))
- buf.WriteString(fmt.Sprintf("| Risiko-Score | %d/100 |\n", a.RiskScore))
- buf.WriteString(fmt.Sprintf("| Komplexität | %s |\n", a.Complexity))
- buf.WriteString(fmt.Sprintf("| DSFA empfohlen | %t |\n", a.DSFARecommended))
- buf.WriteString(fmt.Sprintf("| Art. 22 Risiko | %t |\n", a.Art22Risk))
- buf.WriteString(fmt.Sprintf("| Training erlaubt | %s |\n\n", a.TrainingAllowed))
-
- if len(a.TriggeredRules) > 0 {
- buf.WriteString("## Ausgelöste Regeln\n\n")
- buf.WriteString("| Code | Titel | Schwere | Score |\n")
- buf.WriteString("|------|-------|---------|-------|\n")
- for _, r := range a.TriggeredRules {
- buf.WriteString(fmt.Sprintf("| %s | %s | %s | +%d |\n", r.Code, r.Title, r.Severity, r.ScoreDelta))
- }
- buf.WriteString("\n")
- }
-
- if len(a.RequiredControls) > 0 {
- buf.WriteString("## Erforderliche Kontrollen\n\n")
- for _, c := range a.RequiredControls {
- buf.WriteString(fmt.Sprintf("### %s\n", c.Title))
- buf.WriteString(fmt.Sprintf("%s\n\n", c.Description))
- if c.GDPRRef != "" {
- buf.WriteString(fmt.Sprintf("*Referenz: %s*\n\n", c.GDPRRef))
- }
- }
- }
-
- if len(a.RecommendedArchitecture) > 0 {
- buf.WriteString("## Empfohlene Architektur-Patterns\n\n")
- for _, p := range a.RecommendedArchitecture {
- buf.WriteString(fmt.Sprintf("### %s\n", p.Title))
- buf.WriteString(fmt.Sprintf("%s\n\n", p.Description))
- }
- }
-
- if len(a.ForbiddenPatterns) > 0 {
- buf.WriteString("## Verbotene Patterns\n\n")
- for _, p := range a.ForbiddenPatterns {
- buf.WriteString(fmt.Sprintf("### %s\n", p.Title))
- buf.WriteString(fmt.Sprintf("**Grund:** %s\n\n", p.Reason))
- }
- }
-
- if a.ExplanationText != nil && *a.ExplanationText != "" {
- buf.WriteString("## KI-Erklärung\n\n")
- buf.WriteString(*a.ExplanationText)
- buf.WriteString("\n\n")
- }
-
- buf.WriteString("---\n")
- buf.WriteString(fmt.Sprintf("*Generiert mit UCCA Policy Version %s*\n", a.PolicyVersion))
-
- return buf.String()
-}
-
-// ============================================================================
-// GET /sdk/v1/ucca/stats - Get statistics
-// ============================================================================
-
// GetStats returns UCCA statistics for a tenant
func (h *UCCAHandlers) GetStats(c *gin.Context) {
tenantID := rbac.GetTenantID(c)
@@ -830,306 +263,70 @@ func (h *UCCAHandlers) GetStats(c *gin.Context) {
c.JSON(http.StatusOK, stats)
}
-// ============================================================================
-// POST /sdk/v1/ucca/wizard/ask - Legal Assistant for Wizard
-// ============================================================================
-
-// WizardAskRequest represents a question to the Legal Assistant
-type WizardAskRequest struct {
- Question string `json:"question" binding:"required"`
- StepNumber int `json:"step_number"`
- FieldID string `json:"field_id,omitempty"` // Optional: Specific field context
- CurrentData map[string]interface{} `json:"current_data,omitempty"` // Current wizard answers
-}
-
-// WizardAskResponse represents the Legal Assistant response
-type WizardAskResponse struct {
- Answer string `json:"answer"`
- Sources []LegalSource `json:"sources,omitempty"`
- RelatedFields []string `json:"related_fields,omitempty"`
- GeneratedAt time.Time `json:"generated_at"`
- Model string `json:"model"`
-}
-
-// LegalSource represents a legal reference used in the answer
-type LegalSource struct {
- Regulation string `json:"regulation"`
- Article string `json:"article,omitempty"`
- Text string `json:"text,omitempty"`
-}
-
-// AskWizardQuestion handles legal questions from the wizard
-func (h *UCCAHandlers) AskWizardQuestion(c *gin.Context) {
- var req WizardAskRequest
- if err := c.ShouldBindJSON(&req); err != nil {
- c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
- return
+// createEscalationForAssessment automatically creates an escalation based on assessment result
+func (h *UCCAHandlers) createEscalationForAssessment(c *gin.Context, assessment *ucca.Assessment, result *ucca.AssessmentResult, tenantID, userID uuid.UUID) *ucca.Escalation {
+ if h.escalationStore == nil || h.escalationTrigger == nil {
+ return nil
}
- // Build context-aware query for Legal RAG
- ragQuery := buildWizardRAGQuery(req)
+ level, reason := h.escalationTrigger.DetermineEscalationLevel(result)
- // Search legal corpus for relevant context
- var legalResults []ucca.LegalSearchResult
- var sources []LegalSource
- if h.legalRAGClient != nil {
- results, err := h.legalRAGClient.Search(c.Request.Context(), ragQuery, nil, 5)
- if err != nil {
- // Log but continue without RAG context
- fmt.Printf("Warning: Legal RAG search failed: %v\n", err)
- } else {
- legalResults = results
- // Convert to sources
- sources = make([]LegalSource, len(results))
- for i, r := range results {
- sources[i] = LegalSource{
- Regulation: r.RegulationName,
- Article: r.Article,
- Text: truncateText(r.Text, 200),
- }
- }
- }
+ responseHours, _ := ucca.GetDefaultSLA(level)
+ var dueDate *time.Time
+ if responseHours > 0 {
+ due := time.Now().UTC().Add(time.Duration(responseHours) * time.Hour)
+ dueDate = &due
}
- // Build prompt for LLM
- prompt := buildWizardAssistantPrompt(req, legalResults)
-
- // Build system prompt with step context
- systemPrompt := buildWizardSystemPrompt(req.StepNumber)
-
- // Call LLM
- chatReq := &llm.ChatRequest{
- Messages: []llm.Message{
- {Role: "system", Content: systemPrompt},
- {Role: "user", Content: prompt},
- },
- MaxTokens: 1024,
- Temperature: 0.3, // Low temperature for precise legal answers
- }
- response, err := h.providerRegistry.Chat(c.Request.Context(), chatReq)
- if err != nil {
- c.JSON(http.StatusInternalServerError, gin.H{"error": "LLM call failed: " + err.Error()})
- return
+ escalation := &ucca.Escalation{
+ TenantID: tenantID,
+ AssessmentID: assessment.ID,
+ EscalationLevel: level,
+ EscalationReason: reason,
+ Status: ucca.EscalationStatusPending,
+ DueDate: dueDate,
}
- // Identify related wizard fields based on question
- relatedFields := identifyRelatedFields(req.Question)
+ if level == ucca.EscalationLevelE0 {
+ escalation.Status = ucca.EscalationStatusApproved
+ approveDecision := ucca.EscalationDecisionApprove
+ escalation.Decision = &approveDecision
+ now := time.Now().UTC()
+ escalation.DecisionAt = &now
+ autoNotes := "Automatische Freigabe (E0)"
+ escalation.DecisionNotes = &autoNotes
+ }
- c.JSON(http.StatusOK, WizardAskResponse{
- Answer: response.Message.Content,
- Sources: sources,
- RelatedFields: relatedFields,
- GeneratedAt: time.Now().UTC(),
- Model: response.Model,
+ if err := h.escalationStore.CreateEscalation(c.Request.Context(), escalation); err != nil {
+ fmt.Printf("Warning: Could not create escalation: %v\n", err)
+ return nil
+ }
+
+ h.escalationStore.AddEscalationHistory(c.Request.Context(), &ucca.EscalationHistory{
+ EscalationID: escalation.ID,
+ Action: "auto_created",
+ NewStatus: string(escalation.Status),
+ NewLevel: string(escalation.EscalationLevel),
+ ActorID: userID,
+ Notes: "Automatisch erstellt bei Assessment",
})
-}
-// buildWizardRAGQuery creates an optimized query for Legal RAG search
-func buildWizardRAGQuery(req WizardAskRequest) string {
- // Start with the user's question
- query := req.Question
-
- // Add context based on step number
- stepContext := map[int]string{
- 1: "KI-Anwendung Use Case",
- 2: "personenbezogene Daten Datenkategorien DSGVO Art. 4 Art. 9",
- 3: "Verarbeitungszweck Profiling Scoring automatisierte Entscheidung Art. 22",
- 4: "Hosting Cloud On-Premises Auftragsverarbeitung",
- 5: "Standardvertragsklauseln SCC Drittlandtransfer TIA Transfer Impact Assessment Art. 44 Art. 46",
- 6: "KI-Modell Training RAG Finetuning",
- 7: "Auftragsverarbeitungsvertrag AVV DSFA Verarbeitungsverzeichnis Art. 28 Art. 30 Art. 35",
- 8: "Automatisierung Human-in-the-Loop Art. 22 AI Act",
- }
-
- if context, ok := stepContext[req.StepNumber]; ok {
- query = query + " " + context
- }
-
- return query
-}
-
-// buildWizardSystemPrompt creates the system prompt for the Legal Assistant
-func buildWizardSystemPrompt(stepNumber int) string {
- basePrompt := `Du bist ein freundlicher Rechtsassistent, der Nutzern hilft,
-datenschutzrechtliche Begriffe und Anforderungen zu verstehen.
-
-DEINE AUFGABE:
-- Erkläre rechtliche Begriffe in einfacher, verständlicher Sprache
-- Beantworte Fragen zum aktuellen Wizard-Schritt
-- Hilf dem Nutzer, die richtigen Antworten im Wizard zu geben
-- Verweise auf relevante Rechtsquellen (DSGVO-Artikel, etc.)
-
-WICHTIGE REGELN:
-- Antworte IMMER auf Deutsch
-- Verwende einfache Sprache, keine Juristensprache
-- Gib konkrete Beispiele wenn möglich
-- Bei Unsicherheit empfehle die Rücksprache mit einem Datenschutzbeauftragten
-- Du darfst KEINE Rechtsberatung geben, nur erklären
-
-ANTWORT-FORMAT:
-- Kurz und prägnant (max. 3-4 Sätze für einfache Fragen)
-- Strukturiert mit Aufzählungen bei komplexen Themen
-- Immer mit Quellenangabe am Ende (z.B. "Siehe: DSGVO Art. 9")`
-
- // Add step-specific context
- stepContexts := map[int]string{
- 1: "\n\nKONTEXT: Der Nutzer befindet sich im ersten Schritt und gibt grundlegende Informationen zum KI-Vorhaben ein.",
- 2: "\n\nKONTEXT: Der Nutzer gibt an, welche Datenarten verarbeitet werden. Erkläre die Unterschiede zwischen personenbezogenen Daten, Art. 9 Daten (besondere Kategorien), biometrischen Daten, etc.",
- 3: "\n\nKONTEXT: Der Nutzer gibt den Verarbeitungszweck an. Erkläre Begriffe wie Profiling, Scoring, systematische Überwachung, automatisierte Entscheidungen mit rechtlicher Wirkung.",
- 4: "\n\nKONTEXT: Der Nutzer gibt Hosting-Informationen an. Erkläre Cloud vs. On-Premises, wann Drittlandtransfer vorliegt, Unterschiede zwischen EU/EWR und Drittländern.",
- 5: "\n\nKONTEXT: Der Nutzer beantwortet Fragen zu SCC und TIA. Erkläre Standardvertragsklauseln (SCC), Transfer Impact Assessment (TIA), das Data Privacy Framework (DPF), und wann welche Instrumente erforderlich sind.",
- 6: "\n\nKONTEXT: Der Nutzer gibt KI-Modell-Informationen an. Erkläre RAG vs. Training/Finetuning, warum Training mit personenbezogenen Daten problematisch ist, und welche Opt-Out-Klauseln wichtig sind.",
- 7: "\n\nKONTEXT: Der Nutzer beantwortet Fragen zu Verträgen. Erkläre den Auftragsverarbeitungsvertrag (AVV), die Datenschutz-Folgenabschätzung (DSFA), das Verarbeitungsverzeichnis (VVT), und wann diese erforderlich sind.",
- 8: "\n\nKONTEXT: Der Nutzer gibt den Automatisierungsgrad an. Erkläre Human-in-the-Loop, Art. 22 DSGVO (automatisierte Einzelentscheidungen), und die Anforderungen des AI Acts.",
- }
-
- if context, ok := stepContexts[stepNumber]; ok {
- basePrompt += context
- }
-
- return basePrompt
-}
-
-// buildWizardAssistantPrompt creates the user prompt with legal context
-func buildWizardAssistantPrompt(req WizardAskRequest, legalResults []ucca.LegalSearchResult) string {
- var buf bytes.Buffer
-
- buf.WriteString(fmt.Sprintf("FRAGE DES NUTZERS:\n%s\n\n", req.Question))
-
- // Add legal context if available
- if len(legalResults) > 0 {
- buf.WriteString("RELEVANTE RECHTSGRUNDLAGEN (aus unserer Bibliothek):\n\n")
- for i, result := range legalResults {
- buf.WriteString(fmt.Sprintf("%d. %s", i+1, result.RegulationName))
- if result.Article != "" {
- buf.WriteString(fmt.Sprintf(" - Art. %s", result.Article))
- if result.Paragraph != "" {
- buf.WriteString(fmt.Sprintf(" Abs. %s", result.Paragraph))
- }
- }
- buf.WriteString("\n")
- buf.WriteString(fmt.Sprintf(" %s\n\n", truncateText(result.Text, 300)))
+ if level != ucca.EscalationLevelE0 {
+ role := ucca.GetRoleForLevel(level)
+ reviewer, err := h.escalationStore.GetNextAvailableReviewer(c.Request.Context(), tenantID, role)
+ if err == nil && reviewer != nil {
+ h.escalationStore.AssignEscalation(c.Request.Context(), escalation.ID, reviewer.UserID, role)
+ h.escalationStore.IncrementReviewerCount(c.Request.Context(), reviewer.UserID)
+ h.escalationStore.AddEscalationHistory(c.Request.Context(), &ucca.EscalationHistory{
+ EscalationID: escalation.ID,
+ Action: "auto_assigned",
+ OldStatus: string(ucca.EscalationStatusPending),
+ NewStatus: string(ucca.EscalationStatusAssigned),
+ ActorID: userID,
+ Notes: "Automatisch zugewiesen an: " + reviewer.UserName,
+ })
}
}
- // Add field context if provided
- if req.FieldID != "" {
- buf.WriteString(fmt.Sprintf("AKTUELLES FELD: %s\n\n", req.FieldID))
- }
-
- buf.WriteString("Bitte beantworte die Frage kurz und verständlich. Verwende die angegebenen Rechtsgrundlagen als Referenz.")
-
- return buf.String()
-}
-
-// identifyRelatedFields identifies wizard fields related to the question
-func identifyRelatedFields(question string) []string {
- question = strings.ToLower(question)
- var related []string
-
- // Map keywords to wizard field IDs
- keywordMapping := map[string][]string{
- "personenbezogen": {"data_types.personal_data"},
- "art. 9": {"data_types.article_9_data"},
- "sensibel": {"data_types.article_9_data"},
- "gesundheit": {"data_types.article_9_data"},
- "minderjährig": {"data_types.minor_data"},
- "kinder": {"data_types.minor_data"},
- "biometrisch": {"data_types.biometric_data"},
- "gesicht": {"data_types.biometric_data"},
- "kennzeichen": {"data_types.license_plates"},
- "standort": {"data_types.location_data"},
- "gps": {"data_types.location_data"},
- "profiling": {"purpose.profiling"},
- "scoring": {"purpose.evaluation_scoring"},
- "überwachung": {"processing.systematic_monitoring"},
- "automatisch": {"outputs.decision_with_legal_effect", "automation"},
- "entscheidung": {"outputs.decision_with_legal_effect"},
- "cloud": {"hosting.type", "hosting.region"},
- "on-premises": {"hosting.type"},
- "lokal": {"hosting.type"},
- "scc": {"contracts.scc.present", "contracts.scc.version"},
- "standardvertrags": {"contracts.scc.present"},
- "drittland": {"hosting.region", "provider.location"},
- "usa": {"hosting.region", "provider.location", "provider.dpf_certified"},
- "transfer": {"hosting.region", "contracts.tia.present"},
- "tia": {"contracts.tia.present", "contracts.tia.result"},
- "dpf": {"provider.dpf_certified"},
- "data privacy": {"provider.dpf_certified"},
- "avv": {"contracts.avv.present"},
- "auftragsverarbeitung": {"contracts.avv.present"},
- "dsfa": {"governance.dsfa_completed"},
- "folgenabschätzung": {"governance.dsfa_completed"},
- "verarbeitungsverzeichnis": {"governance.vvt_entry"},
- "training": {"model_usage.training", "provider.uses_data_for_training"},
- "finetuning": {"model_usage.training"},
- "rag": {"model_usage.rag"},
- "human": {"processing.human_oversight"},
- "aufsicht": {"processing.human_oversight"},
- }
-
- seen := make(map[string]bool)
- for keyword, fields := range keywordMapping {
- if strings.Contains(question, keyword) {
- for _, field := range fields {
- if !seen[field] {
- related = append(related, field)
- seen[field] = true
- }
- }
- }
- }
-
- return related
-}
-
-// ============================================================================
-// GET /sdk/v1/ucca/wizard/schema - Get Wizard Schema
-// ============================================================================
-
-// GetWizardSchema returns the wizard schema for the frontend
-func (h *UCCAHandlers) GetWizardSchema(c *gin.Context) {
- // For now, return a static schema info
- // In future, this could be loaded from the YAML file
- c.JSON(http.StatusOK, gin.H{
- "version": "1.1",
- "total_steps": 8,
- "default_mode": "simple",
- "legal_assistant": gin.H{
- "enabled": true,
- "endpoint": "/sdk/v1/ucca/wizard/ask",
- "max_tokens": 1024,
- "example_questions": []string{
- "Was sind personenbezogene Daten?",
- "Was ist der Unterschied zwischen AVV und SCC?",
- "Brauche ich ein TIA?",
- "Was bedeutet Profiling?",
- "Was ist Art. 9 DSGVO?",
- "Wann brauche ich eine DSFA?",
- "Was ist das Data Privacy Framework?",
- },
- },
- "steps": []gin.H{
- {"number": 1, "title": "Grundlegende Informationen", "icon": "info"},
- {"number": 2, "title": "Welche Daten werden verarbeitet?", "icon": "database"},
- {"number": 3, "title": "Wofür wird die KI eingesetzt?", "icon": "target"},
- {"number": 4, "title": "Wo läuft die KI?", "icon": "server"},
- {"number": 5, "title": "Internationaler Datentransfer", "icon": "globe"},
- {"number": 6, "title": "KI-Modell und Training", "icon": "brain"},
- {"number": 7, "title": "Verträge & Compliance", "icon": "file-contract"},
- {"number": 8, "title": "Automatisierung & Kontrolle", "icon": "user-check"},
- },
- })
-}
-
-// ============================================================================
-// Helper functions
-// ============================================================================
-
-// truncateText truncates a string to maxLen characters
-func truncateText(text string, maxLen int) string {
- if len(text) <= maxLen {
- return text
- }
- return text[:maxLen] + "..."
+ return escalation
}
diff --git a/ai-compliance-sdk/internal/api/handlers/ucca_handlers_catalog.go b/ai-compliance-sdk/internal/api/handlers/ucca_handlers_catalog.go
new file mode 100644
index 0000000..4e08015
--- /dev/null
+++ b/ai-compliance-sdk/internal/api/handlers/ucca_handlers_catalog.go
@@ -0,0 +1,203 @@
+package handlers
+
+import (
+ "net/http"
+
+ "github.com/breakpilot/ai-compliance-sdk/internal/ucca"
+ "github.com/gin-gonic/gin"
+)
+
+// ListPatterns returns all available architecture patterns
+func (h *UCCAHandlers) ListPatterns(c *gin.Context) {
+ var response []gin.H
+
+ if h.policyEngine != nil {
+ yamlPatterns := h.policyEngine.GetAllPatterns()
+ response = make([]gin.H, 0, len(yamlPatterns))
+ for _, p := range yamlPatterns {
+ response = append(response, gin.H{
+ "id": p.ID,
+ "title": p.Title,
+ "description": p.Description,
+ "benefit": p.Benefit,
+ "effort": p.Effort,
+ "risk_reduction": p.RiskReduction,
+ })
+ }
+ } else {
+ patterns := ucca.GetAllPatterns()
+ response = make([]gin.H, len(patterns))
+ for i, p := range patterns {
+ response[i] = gin.H{
+ "id": p.ID,
+ "title": p.Title,
+ "title_de": p.TitleDE,
+ "description": p.Description,
+ "description_de": p.DescriptionDE,
+ "benefits": p.Benefits,
+ "requirements": p.Requirements,
+ }
+ }
+ }
+
+ c.JSON(http.StatusOK, gin.H{"patterns": response})
+}
+
+// ListControls returns all available compliance controls
+func (h *UCCAHandlers) ListControls(c *gin.Context) {
+ var response []gin.H
+
+ if h.policyEngine != nil {
+ yamlControls := h.policyEngine.GetAllControls()
+ response = make([]gin.H, 0, len(yamlControls))
+ for _, ctrl := range yamlControls {
+ response = append(response, gin.H{
+ "id": ctrl.ID,
+ "title": ctrl.Title,
+ "description": ctrl.Description,
+ "gdpr_ref": ctrl.GDPRRef,
+ "effort": ctrl.Effort,
+ })
+ }
+ } else {
+ for id, ctrl := range ucca.ControlLibrary {
+ response = append(response, gin.H{
+ "id": id,
+ "title": ctrl.Title,
+ "description": ctrl.Description,
+ "severity": ctrl.Severity,
+ "category": ctrl.Category,
+ "gdpr_ref": ctrl.GDPRRef,
+ })
+ }
+ }
+
+ c.JSON(http.StatusOK, gin.H{"controls": response})
+}
+
+// ListProblemSolutions returns all problem-solution mappings
+func (h *UCCAHandlers) ListProblemSolutions(c *gin.Context) {
+ if h.policyEngine == nil {
+ c.JSON(http.StatusOK, gin.H{
+ "problem_solutions": []gin.H{},
+ "message": "Problem-solutions only available with YAML policy engine",
+ })
+ return
+ }
+
+ problemSolutions := h.policyEngine.GetProblemSolutions()
+ response := make([]gin.H, len(problemSolutions))
+ for i, ps := range problemSolutions {
+ solutions := make([]gin.H, len(ps.Solutions))
+ for j, sol := range ps.Solutions {
+ solutions[j] = gin.H{
+ "id": sol.ID,
+ "title": sol.Title,
+ "pattern": sol.Pattern,
+ "control": sol.Control,
+ "removes_problem": sol.RemovesProblem,
+ "team_question": sol.TeamQuestion,
+ }
+ }
+
+ triggers := make([]gin.H, len(ps.Triggers))
+ for j, t := range ps.Triggers {
+ triggers[j] = gin.H{
+ "rule": t.Rule,
+ "without_control": t.WithoutControl,
+ }
+ }
+
+ response[i] = gin.H{
+ "problem_id": ps.ProblemID,
+ "title": ps.Title,
+ "triggers": triggers,
+ "solutions": solutions,
+ }
+ }
+
+ c.JSON(http.StatusOK, gin.H{"problem_solutions": response})
+}
+
+// ListExamples returns all available didactic examples
+func (h *UCCAHandlers) ListExamples(c *gin.Context) {
+ examples := ucca.GetAllExamples()
+
+ response := make([]gin.H, len(examples))
+ for i, ex := range examples {
+ response[i] = gin.H{
+ "id": ex.ID,
+ "title": ex.Title,
+ "title_de": ex.TitleDE,
+ "description": ex.Description,
+ "description_de": ex.DescriptionDE,
+ "domain": ex.Domain,
+ "outcome": ex.Outcome,
+ "outcome_de": ex.OutcomeDE,
+ "lessons": ex.Lessons,
+ "lessons_de": ex.LessonsDE,
+ }
+ }
+
+ c.JSON(http.StatusOK, gin.H{"examples": response})
+}
+
+// ListRules returns all rules for transparency
+func (h *UCCAHandlers) ListRules(c *gin.Context) {
+ var response []gin.H
+ var policyVersion string
+
+ if h.policyEngine != nil {
+ yamlRules := h.policyEngine.GetAllRules()
+ policyVersion = h.policyEngine.GetPolicyVersion()
+ response = make([]gin.H, len(yamlRules))
+ for i, r := range yamlRules {
+ response[i] = gin.H{
+ "code": r.ID,
+ "category": r.Category,
+ "title": r.Title,
+ "description": r.Description,
+ "severity": r.Severity,
+ "gdpr_ref": r.GDPRRef,
+ "rationale": r.Rationale,
+ "controls": r.Effect.ControlsAdd,
+ "patterns": r.Effect.SuggestedPatterns,
+ "risk_add": r.Effect.RiskAdd,
+ }
+ }
+ } else {
+ rules := h.legacyRuleEngine.GetRules()
+ policyVersion = "1.0.0-legacy"
+ response = make([]gin.H, len(rules))
+ for i, r := range rules {
+ response[i] = gin.H{
+ "code": r.Code,
+ "category": r.Category,
+ "title": r.Title,
+ "title_de": r.TitleDE,
+ "description": r.Description,
+ "description_de": r.DescriptionDE,
+ "severity": r.Severity,
+ "score_delta": r.ScoreDelta,
+ "gdpr_ref": r.GDPRRef,
+ "controls": r.Controls,
+ "patterns": r.Patterns,
+ }
+ }
+ }
+
+ c.JSON(http.StatusOK, gin.H{
+ "rules": response,
+ "total": len(response),
+ "policy_version": policyVersion,
+ "categories": []string{
+ "A. Datenklassifikation",
+ "B. Zweck & Rechtsgrundlage",
+ "C. Automatisierung",
+ "D. Training & Modell",
+ "E. Hosting",
+ "F. Domain-spezifisch",
+ "G. Aggregation",
+ },
+ })
+}
diff --git a/ai-compliance-sdk/internal/api/handlers/ucca_handlers_explain.go b/ai-compliance-sdk/internal/api/handlers/ucca_handlers_explain.go
new file mode 100644
index 0000000..bb3d19c
--- /dev/null
+++ b/ai-compliance-sdk/internal/api/handlers/ucca_handlers_explain.go
@@ -0,0 +1,243 @@
+package handlers
+
+import (
+ "bytes"
+ "fmt"
+ "net/http"
+ "time"
+
+ "github.com/breakpilot/ai-compliance-sdk/internal/llm"
+ "github.com/breakpilot/ai-compliance-sdk/internal/ucca"
+ "github.com/gin-gonic/gin"
+ "github.com/google/uuid"
+)
+
+// Explain generates an LLM explanation for an assessment
+func (h *UCCAHandlers) Explain(c *gin.Context) {
+ id, err := uuid.Parse(c.Param("id"))
+ if err != nil {
+ c.JSON(http.StatusBadRequest, gin.H{"error": "invalid ID"})
+ return
+ }
+
+ var req ucca.ExplainRequest
+ if err := c.ShouldBindJSON(&req); err != nil {
+ req.Language = "de"
+ }
+ if req.Language == "" {
+ req.Language = "de"
+ }
+
+ assessment, err := h.store.GetAssessment(c.Request.Context(), id)
+ if err != nil {
+ c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
+ return
+ }
+ if assessment == nil {
+ c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
+ return
+ }
+
+ // Get legal context from RAG
+ var legalContext *ucca.LegalContext
+ var legalContextStr string
+ if h.legalRAGClient != nil {
+ legalContext, err = h.legalRAGClient.GetLegalContextForAssessment(c.Request.Context(), assessment)
+ if err != nil {
+ fmt.Printf("Warning: Could not get legal context: %v\n", err)
+ } else {
+ legalContextStr = h.legalRAGClient.FormatLegalContextForPrompt(legalContext)
+ }
+ }
+
+ prompt := buildExplanationPrompt(assessment, req.Language, legalContextStr)
+
+ chatReq := &llm.ChatRequest{
+ Messages: []llm.Message{
+ {Role: "system", Content: "Du bist ein Datenschutz-Experte, der DSGVO-Compliance-Bewertungen erklärt. Antworte klar, präzise und auf Deutsch. Beziehe dich auf die angegebenen Rechtsgrundlagen."},
+ {Role: "user", Content: prompt},
+ },
+ MaxTokens: 2000,
+ Temperature: 0.3,
+ }
+ response, err := h.providerRegistry.Chat(c.Request.Context(), chatReq)
+ if err != nil {
+ c.JSON(http.StatusInternalServerError, gin.H{"error": "LLM call failed: " + err.Error()})
+ return
+ }
+
+ explanation := response.Message.Content
+ model := response.Model
+
+ if err := h.store.UpdateExplanation(c.Request.Context(), id, explanation, model); err != nil {
+ c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
+ return
+ }
+
+ c.JSON(http.StatusOK, ucca.ExplainResponse{
+ ExplanationText: explanation,
+ GeneratedAt: time.Now().UTC(),
+ Model: model,
+ LegalContext: legalContext,
+ })
+}
+
+// buildExplanationPrompt creates the prompt for the LLM explanation
+func buildExplanationPrompt(assessment *ucca.Assessment, language string, legalContext string) string {
+ var buf bytes.Buffer
+
+ buf.WriteString("Erkläre die folgende DSGVO-Compliance-Bewertung für einen KI-Use-Case in verständlicher Sprache:\n\n")
+
+ buf.WriteString(fmt.Sprintf("**Ergebnis:** %s\n", assessment.Feasibility))
+ buf.WriteString(fmt.Sprintf("**Risikostufe:** %s\n", assessment.RiskLevel))
+ buf.WriteString(fmt.Sprintf("**Risiko-Score:** %d/100\n", assessment.RiskScore))
+ buf.WriteString(fmt.Sprintf("**Komplexität:** %s\n\n", assessment.Complexity))
+
+ if len(assessment.TriggeredRules) > 0 {
+ buf.WriteString("**Ausgelöste Regeln:**\n")
+ for _, r := range assessment.TriggeredRules {
+ buf.WriteString(fmt.Sprintf("- %s (%s): %s\n", r.Code, r.Severity, r.Title))
+ }
+ buf.WriteString("\n")
+ }
+
+ if len(assessment.RequiredControls) > 0 {
+ buf.WriteString("**Erforderliche Maßnahmen:**\n")
+ for _, ctrl := range assessment.RequiredControls {
+ buf.WriteString(fmt.Sprintf("- %s: %s\n", ctrl.Title, ctrl.Description))
+ }
+ buf.WriteString("\n")
+ }
+
+ if assessment.DSFARecommended {
+ buf.WriteString("**Hinweis:** Eine Datenschutz-Folgenabschätzung (DSFA) wird empfohlen.\n\n")
+ }
+
+ if assessment.Art22Risk {
+ buf.WriteString("**Warnung:** Es besteht ein Risiko unter Art. 22 DSGVO (automatisierte Einzelentscheidungen).\n\n")
+ }
+
+ if legalContext != "" {
+ buf.WriteString(legalContext)
+ }
+
+ buf.WriteString("\nBitte erkläre:\n")
+ buf.WriteString("1. Warum dieses Ergebnis zustande kam (mit Bezug auf die angegebenen Rechtsgrundlagen)\n")
+ buf.WriteString("2. Welche konkreten Schritte unternommen werden sollten\n")
+ buf.WriteString("3. Welche Alternativen es gibt, falls der Use Case abgelehnt wurde\n")
+ buf.WriteString("4. Welche spezifischen Artikel aus DSGVO/AI Act beachtet werden müssen\n")
+
+ return buf.String()
+}
+
+// Export exports an assessment as JSON or Markdown
+func (h *UCCAHandlers) Export(c *gin.Context) {
+ id, err := uuid.Parse(c.Param("id"))
+ if err != nil {
+ c.JSON(http.StatusBadRequest, gin.H{"error": "invalid ID"})
+ return
+ }
+
+ format := c.DefaultQuery("format", "json")
+
+ assessment, err := h.store.GetAssessment(c.Request.Context(), id)
+ if err != nil {
+ c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
+ return
+ }
+ if assessment == nil {
+ c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
+ return
+ }
+
+ if format == "md" {
+ markdown := generateMarkdownExport(assessment)
+ c.Header("Content-Type", "text/markdown; charset=utf-8")
+ c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=ucca_assessment_%s.md", id.String()[:8]))
+ c.Data(http.StatusOK, "text/markdown; charset=utf-8", []byte(markdown))
+ return
+ }
+
+ c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=ucca_assessment_%s.json", id.String()[:8]))
+ c.JSON(http.StatusOK, gin.H{
+ "exported_at": time.Now().UTC().Format(time.RFC3339),
+ "assessment": assessment,
+ })
+}
+
+// generateMarkdownExport creates a Markdown export of the assessment
+func generateMarkdownExport(a *ucca.Assessment) string {
+ var buf bytes.Buffer
+
+ buf.WriteString("# UCCA Use-Case Assessment\n\n")
+ buf.WriteString(fmt.Sprintf("**ID:** %s\n", a.ID.String()))
+ buf.WriteString(fmt.Sprintf("**Erstellt:** %s\n", a.CreatedAt.Format("02.01.2006 15:04")))
+ buf.WriteString(fmt.Sprintf("**Domain:** %s\n\n", a.Domain))
+
+ buf.WriteString("## Ergebnis\n\n")
+ buf.WriteString("| Kriterium | Wert |\n")
+ buf.WriteString("|-----------|------|\n")
+ buf.WriteString(fmt.Sprintf("| Machbarkeit | **%s** |\n", a.Feasibility))
+ buf.WriteString(fmt.Sprintf("| Risikostufe | %s |\n", a.RiskLevel))
+ buf.WriteString(fmt.Sprintf("| Risiko-Score | %d/100 |\n", a.RiskScore))
+ buf.WriteString(fmt.Sprintf("| Komplexität | %s |\n", a.Complexity))
+ buf.WriteString(fmt.Sprintf("| DSFA empfohlen | %t |\n", a.DSFARecommended))
+ buf.WriteString(fmt.Sprintf("| Art. 22 Risiko | %t |\n", a.Art22Risk))
+ buf.WriteString(fmt.Sprintf("| Training erlaubt | %s |\n\n", a.TrainingAllowed))
+
+ if len(a.TriggeredRules) > 0 {
+ buf.WriteString("## Ausgelöste Regeln\n\n")
+ buf.WriteString("| Code | Titel | Schwere | Score |\n")
+ buf.WriteString("|------|-------|---------|-------|\n")
+ for _, r := range a.TriggeredRules {
+ buf.WriteString(fmt.Sprintf("| %s | %s | %s | +%d |\n", r.Code, r.Title, r.Severity, r.ScoreDelta))
+ }
+ buf.WriteString("\n")
+ }
+
+ if len(a.RequiredControls) > 0 {
+ buf.WriteString("## Erforderliche Kontrollen\n\n")
+ for _, ctrl := range a.RequiredControls {
+ buf.WriteString(fmt.Sprintf("### %s\n", ctrl.Title))
+ buf.WriteString(fmt.Sprintf("%s\n\n", ctrl.Description))
+ if ctrl.GDPRRef != "" {
+ buf.WriteString(fmt.Sprintf("*Referenz: %s*\n\n", ctrl.GDPRRef))
+ }
+ }
+ }
+
+ if len(a.RecommendedArchitecture) > 0 {
+ buf.WriteString("## Empfohlene Architektur-Patterns\n\n")
+ for _, p := range a.RecommendedArchitecture {
+ buf.WriteString(fmt.Sprintf("### %s\n", p.Title))
+ buf.WriteString(fmt.Sprintf("%s\n\n", p.Description))
+ }
+ }
+
+ if len(a.ForbiddenPatterns) > 0 {
+ buf.WriteString("## Verbotene Patterns\n\n")
+ for _, p := range a.ForbiddenPatterns {
+ buf.WriteString(fmt.Sprintf("### %s\n", p.Title))
+ buf.WriteString(fmt.Sprintf("**Grund:** %s\n\n", p.Reason))
+ }
+ }
+
+ if a.ExplanationText != nil && *a.ExplanationText != "" {
+ buf.WriteString("## KI-Erklärung\n\n")
+ buf.WriteString(*a.ExplanationText)
+ buf.WriteString("\n\n")
+ }
+
+ buf.WriteString("---\n")
+ buf.WriteString(fmt.Sprintf("*Generiert mit UCCA Policy Version %s*\n", a.PolicyVersion))
+
+ return buf.String()
+}
+
+// truncateText truncates a string to maxLen characters
+func truncateText(text string, maxLen int) string {
+ if len(text) <= maxLen {
+ return text
+ }
+ return text[:maxLen] + "..."
+}
diff --git a/ai-compliance-sdk/internal/api/handlers/ucca_handlers_wizard.go b/ai-compliance-sdk/internal/api/handlers/ucca_handlers_wizard.go
new file mode 100644
index 0000000..ded0553
--- /dev/null
+++ b/ai-compliance-sdk/internal/api/handlers/ucca_handlers_wizard.go
@@ -0,0 +1,280 @@
+package handlers
+
+import (
+ "bytes"
+ "fmt"
+ "net/http"
+ "strings"
+ "time"
+
+ "github.com/breakpilot/ai-compliance-sdk/internal/llm"
+ "github.com/breakpilot/ai-compliance-sdk/internal/ucca"
+ "github.com/gin-gonic/gin"
+)
+
+// WizardAskRequest represents a question to the Legal Assistant
+type WizardAskRequest struct {
+ Question string `json:"question" binding:"required"`
+ StepNumber int `json:"step_number"`
+ FieldID string `json:"field_id,omitempty"`
+ CurrentData map[string]interface{} `json:"current_data,omitempty"`
+}
+
+// WizardAskResponse represents the Legal Assistant response
+type WizardAskResponse struct {
+ Answer string `json:"answer"`
+ Sources []LegalSource `json:"sources,omitempty"`
+ RelatedFields []string `json:"related_fields,omitempty"`
+ GeneratedAt time.Time `json:"generated_at"`
+ Model string `json:"model"`
+}
+
+// LegalSource represents a legal reference used in the answer
+type LegalSource struct {
+ Regulation string `json:"regulation"`
+ Article string `json:"article,omitempty"`
+ Text string `json:"text,omitempty"`
+}
+
+// AskWizardQuestion handles legal questions from the wizard
+func (h *UCCAHandlers) AskWizardQuestion(c *gin.Context) {
+ var req WizardAskRequest
+ if err := c.ShouldBindJSON(&req); err != nil {
+ c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
+ return
+ }
+
+ ragQuery := buildWizardRAGQuery(req)
+
+ var legalResults []ucca.LegalSearchResult
+ var sources []LegalSource
+ if h.legalRAGClient != nil {
+ results, err := h.legalRAGClient.Search(c.Request.Context(), ragQuery, nil, 5)
+ if err != nil {
+ fmt.Printf("Warning: Legal RAG search failed: %v\n", err)
+ } else {
+ legalResults = results
+ sources = make([]LegalSource, len(results))
+ for i, r := range results {
+ sources[i] = LegalSource{
+ Regulation: r.RegulationName,
+ Article: r.Article,
+ Text: truncateText(r.Text, 200),
+ }
+ }
+ }
+ }
+
+ prompt := buildWizardAssistantPrompt(req, legalResults)
+ systemPrompt := buildWizardSystemPrompt(req.StepNumber)
+
+ chatReq := &llm.ChatRequest{
+ Messages: []llm.Message{
+ {Role: "system", Content: systemPrompt},
+ {Role: "user", Content: prompt},
+ },
+ MaxTokens: 1024,
+ Temperature: 0.3,
+ }
+ response, err := h.providerRegistry.Chat(c.Request.Context(), chatReq)
+ if err != nil {
+ c.JSON(http.StatusInternalServerError, gin.H{"error": "LLM call failed: " + err.Error()})
+ return
+ }
+
+ relatedFields := identifyRelatedFields(req.Question)
+
+ c.JSON(http.StatusOK, WizardAskResponse{
+ Answer: response.Message.Content,
+ Sources: sources,
+ RelatedFields: relatedFields,
+ GeneratedAt: time.Now().UTC(),
+ Model: response.Model,
+ })
+}
+
+// GetWizardSchema returns the wizard schema for the frontend
+func (h *UCCAHandlers) GetWizardSchema(c *gin.Context) {
+ c.JSON(http.StatusOK, gin.H{
+ "version": "1.1",
+ "total_steps": 8,
+ "default_mode": "simple",
+ "legal_assistant": gin.H{
+ "enabled": true,
+ "endpoint": "/sdk/v1/ucca/wizard/ask",
+ "max_tokens": 1024,
+ "example_questions": []string{
+ "Was sind personenbezogene Daten?",
+ "Was ist der Unterschied zwischen AVV und SCC?",
+ "Brauche ich ein TIA?",
+ "Was bedeutet Profiling?",
+ "Was ist Art. 9 DSGVO?",
+ "Wann brauche ich eine DSFA?",
+ "Was ist das Data Privacy Framework?",
+ },
+ },
+ "steps": []gin.H{
+ {"number": 1, "title": "Grundlegende Informationen", "icon": "info"},
+ {"number": 2, "title": "Welche Daten werden verarbeitet?", "icon": "database"},
+ {"number": 3, "title": "Wofür wird die KI eingesetzt?", "icon": "target"},
+ {"number": 4, "title": "Wo läuft die KI?", "icon": "server"},
+ {"number": 5, "title": "Internationaler Datentransfer", "icon": "globe"},
+ {"number": 6, "title": "KI-Modell und Training", "icon": "brain"},
+ {"number": 7, "title": "Verträge & Compliance", "icon": "file-contract"},
+ {"number": 8, "title": "Automatisierung & Kontrolle", "icon": "user-check"},
+ },
+ })
+}
+
+// buildWizardRAGQuery creates an optimized query for Legal RAG search
+func buildWizardRAGQuery(req WizardAskRequest) string {
+ query := req.Question
+
+ stepContext := map[int]string{
+ 1: "KI-Anwendung Use Case",
+ 2: "personenbezogene Daten Datenkategorien DSGVO Art. 4 Art. 9",
+ 3: "Verarbeitungszweck Profiling Scoring automatisierte Entscheidung Art. 22",
+ 4: "Hosting Cloud On-Premises Auftragsverarbeitung",
+ 5: "Standardvertragsklauseln SCC Drittlandtransfer TIA Transfer Impact Assessment Art. 44 Art. 46",
+ 6: "KI-Modell Training RAG Finetuning",
+ 7: "Auftragsverarbeitungsvertrag AVV DSFA Verarbeitungsverzeichnis Art. 28 Art. 30 Art. 35",
+ 8: "Automatisierung Human-in-the-Loop Art. 22 AI Act",
+ }
+
+ if context, ok := stepContext[req.StepNumber]; ok {
+ query = query + " " + context
+ }
+
+ return query
+}
+
+// buildWizardSystemPrompt creates the system prompt for the Legal Assistant
+func buildWizardSystemPrompt(stepNumber int) string {
+ basePrompt := `Du bist ein freundlicher Rechtsassistent, der Nutzern hilft,
+datenschutzrechtliche Begriffe und Anforderungen zu verstehen.
+
+DEINE AUFGABE:
+- Erkläre rechtliche Begriffe in einfacher, verständlicher Sprache
+- Beantworte Fragen zum aktuellen Wizard-Schritt
+- Hilf dem Nutzer, die richtigen Antworten im Wizard zu geben
+- Verweise auf relevante Rechtsquellen (DSGVO-Artikel, etc.)
+
+WICHTIGE REGELN:
+- Antworte IMMER auf Deutsch
+- Verwende einfache Sprache, keine Juristensprache
+- Gib konkrete Beispiele wenn möglich
+- Bei Unsicherheit empfehle die Rücksprache mit einem Datenschutzbeauftragten
+- Du darfst KEINE Rechtsberatung geben, nur erklären
+
+ANTWORT-FORMAT:
+- Kurz und prägnant (max. 3-4 Sätze für einfache Fragen)
+- Strukturiert mit Aufzählungen bei komplexen Themen
+- Immer mit Quellenangabe am Ende (z.B. "Siehe: DSGVO Art. 9")`
+
+ stepContexts := map[int]string{
+ 1: "\n\nKONTEXT: Der Nutzer befindet sich im ersten Schritt und gibt grundlegende Informationen zum KI-Vorhaben ein.",
+ 2: "\n\nKONTEXT: Der Nutzer gibt an, welche Datenarten verarbeitet werden. Erkläre die Unterschiede zwischen personenbezogenen Daten, Art. 9 Daten (besondere Kategorien), biometrischen Daten, etc.",
+ 3: "\n\nKONTEXT: Der Nutzer gibt den Verarbeitungszweck an. Erkläre Begriffe wie Profiling, Scoring, systematische Überwachung, automatisierte Entscheidungen mit rechtlicher Wirkung.",
+ 4: "\n\nKONTEXT: Der Nutzer gibt Hosting-Informationen an. Erkläre Cloud vs. On-Premises, wann Drittlandtransfer vorliegt, Unterschiede zwischen EU/EWR und Drittländern.",
+ 5: "\n\nKONTEXT: Der Nutzer beantwortet Fragen zu SCC und TIA. Erkläre Standardvertragsklauseln (SCC), Transfer Impact Assessment (TIA), das Data Privacy Framework (DPF), und wann welche Instrumente erforderlich sind.",
+ 6: "\n\nKONTEXT: Der Nutzer gibt KI-Modell-Informationen an. Erkläre RAG vs. Training/Finetuning, warum Training mit personenbezogenen Daten problematisch ist, und welche Opt-Out-Klauseln wichtig sind.",
+ 7: "\n\nKONTEXT: Der Nutzer beantwortet Fragen zu Verträgen. Erkläre den Auftragsverarbeitungsvertrag (AVV), die Datenschutz-Folgenabschätzung (DSFA), das Verarbeitungsverzeichnis (VVT), und wann diese erforderlich sind.",
+ 8: "\n\nKONTEXT: Der Nutzer gibt den Automatisierungsgrad an. Erkläre Human-in-the-Loop, Art. 22 DSGVO (automatisierte Einzelentscheidungen), und die Anforderungen des AI Acts.",
+ }
+
+ if context, ok := stepContexts[stepNumber]; ok {
+ basePrompt += context
+ }
+
+ return basePrompt
+}
+
+// buildWizardAssistantPrompt creates the user prompt with legal context
+func buildWizardAssistantPrompt(req WizardAskRequest, legalResults []ucca.LegalSearchResult) string {
+ var buf bytes.Buffer
+
+ buf.WriteString(fmt.Sprintf("FRAGE DES NUTZERS:\n%s\n\n", req.Question))
+
+ if len(legalResults) > 0 {
+ buf.WriteString("RELEVANTE RECHTSGRUNDLAGEN (aus unserer Bibliothek):\n\n")
+ for i, result := range legalResults {
+ buf.WriteString(fmt.Sprintf("%d. %s", i+1, result.RegulationName))
+ if result.Article != "" {
+ buf.WriteString(fmt.Sprintf(" - Art. %s", result.Article))
+ if result.Paragraph != "" {
+ buf.WriteString(fmt.Sprintf(" Abs. %s", result.Paragraph))
+ }
+ }
+ buf.WriteString("\n")
+ buf.WriteString(fmt.Sprintf(" %s\n\n", truncateText(result.Text, 300)))
+ }
+ }
+
+ if req.FieldID != "" {
+ buf.WriteString(fmt.Sprintf("AKTUELLES FELD: %s\n\n", req.FieldID))
+ }
+
+ buf.WriteString("Bitte beantworte die Frage kurz und verständlich. Verwende die angegebenen Rechtsgrundlagen als Referenz.")
+
+ return buf.String()
+}
+
+// identifyRelatedFields identifies wizard fields related to the question
+func identifyRelatedFields(question string) []string {
+ question = strings.ToLower(question)
+ var related []string
+
+ keywordMapping := map[string][]string{
+ "personenbezogen": {"data_types.personal_data"},
+ "art. 9": {"data_types.article_9_data"},
+ "sensibel": {"data_types.article_9_data"},
+ "gesundheit": {"data_types.article_9_data"},
+ "minderjährig": {"data_types.minor_data"},
+ "kinder": {"data_types.minor_data"},
+ "biometrisch": {"data_types.biometric_data"},
+ "gesicht": {"data_types.biometric_data"},
+ "kennzeichen": {"data_types.license_plates"},
+ "standort": {"data_types.location_data"},
+ "gps": {"data_types.location_data"},
+ "profiling": {"purpose.profiling"},
+ "scoring": {"purpose.evaluation_scoring"},
+ "überwachung": {"processing.systematic_monitoring"},
+ "automatisch": {"outputs.decision_with_legal_effect", "automation"},
+ "entscheidung": {"outputs.decision_with_legal_effect"},
+ "cloud": {"hosting.type", "hosting.region"},
+ "on-premises": {"hosting.type"},
+ "lokal": {"hosting.type"},
+ "scc": {"contracts.scc.present", "contracts.scc.version"},
+ "standardvertrags": {"contracts.scc.present"},
+ "drittland": {"hosting.region", "provider.location"},
+ "usa": {"hosting.region", "provider.location", "provider.dpf_certified"},
+ "transfer": {"hosting.region", "contracts.tia.present"},
+ "tia": {"contracts.tia.present", "contracts.tia.result"},
+ "dpf": {"provider.dpf_certified"},
+ "data privacy": {"provider.dpf_certified"},
+ "avv": {"contracts.avv.present"},
+ "auftragsverarbeitung": {"contracts.avv.present"},
+ "dsfa": {"governance.dsfa_completed"},
+ "folgenabschätzung": {"governance.dsfa_completed"},
+ "verarbeitungsverzeichnis": {"governance.vvt_entry"},
+ "training": {"model_usage.training", "provider.uses_data_for_training"},
+ "finetuning": {"model_usage.training"},
+ "rag": {"model_usage.rag"},
+ "human": {"processing.human_oversight"},
+ "aufsicht": {"processing.human_oversight"},
+ }
+
+ seen := make(map[string]bool)
+ for keyword, fields := range keywordMapping {
+ if strings.Contains(question, keyword) {
+ for _, field := range fields {
+ if !seen[field] {
+ related = append(related, field)
+ seen[field] = true
+ }
+ }
+ }
+ }
+
+ return related
+}
diff --git a/ai-compliance-sdk/internal/iace/document_export.go b/ai-compliance-sdk/internal/iace/document_export.go
index 9203444..c9346b5 100644
--- a/ai-compliance-sdk/internal/iace/document_export.go
+++ b/ai-compliance-sdk/internal/iace/document_export.go
@@ -1,16 +1,11 @@
package iace
import (
- "archive/zip"
"bytes"
- "encoding/json"
- "encoding/xml"
"fmt"
- "strings"
"time"
"github.com/jung-kurt/gofpdf"
- "github.com/xuri/excelize/v2"
)
// ExportFormat represents a supported document export format
@@ -50,7 +45,6 @@ func (e *DocumentExporter) ExportPDF(
}
pdf := gofpdf.New("P", "mm", "A4", "")
- pdf.SetFont("Helvetica", "", 12)
// --- Cover Page ---
pdf.AddPage()
@@ -101,599 +95,6 @@ func (e *DocumentExporter) ExportPDF(
return buf.Bytes(), nil
}
-func (e *DocumentExporter) pdfCoverPage(pdf *gofpdf.Fpdf, project *Project) {
- pdf.Ln(60)
-
- // Machine name (large, bold, centered)
- pdf.SetFont("Helvetica", "B", 28)
- pdf.SetTextColor(0, 0, 0)
- pdf.CellFormat(0, 15, "CE-Technische Akte", "", 1, "C", false, 0, "")
- pdf.Ln(5)
-
- pdf.SetFont("Helvetica", "B", 22)
- pdf.CellFormat(0, 12, project.MachineName, "", 1, "C", false, 0, "")
- pdf.Ln(15)
-
- // Metadata block
- pdf.SetFont("Helvetica", "", 12)
- coverItems := []struct {
- label string
- value string
- }{
- {"Hersteller", project.Manufacturer},
- {"Maschinentyp", project.MachineType},
- {"CE-Kennzeichnungsziel", project.CEMarkingTarget},
- {"Projektstatus", string(project.Status)},
- {"Datum", time.Now().Format("02.01.2006")},
- }
-
- for _, item := range coverItems {
- if item.value == "" {
- continue
- }
- pdf.SetFont("Helvetica", "B", 12)
- pdf.CellFormat(60, 8, item.label+":", "", 0, "R", false, 0, "")
- pdf.SetFont("Helvetica", "", 12)
- pdf.CellFormat(5, 8, "", "", 0, "", false, 0, "")
- pdf.CellFormat(0, 8, item.value, "", 1, "L", false, 0, "")
- }
-
- if project.Description != "" {
- pdf.Ln(15)
- pdf.SetFont("Helvetica", "I", 10)
- pdf.MultiCell(0, 5, project.Description, "", "C", false)
- }
-}
-
-func (e *DocumentExporter) pdfTableOfContents(pdf *gofpdf.Fpdf, sections []TechFileSection) {
- pdf.SetFont("Helvetica", "B", 16)
- pdf.SetTextColor(50, 50, 50)
- pdf.CellFormat(0, 10, "Inhaltsverzeichnis", "", 1, "L", false, 0, "")
- pdf.SetTextColor(0, 0, 0)
- pdf.SetDrawColor(200, 200, 200)
- pdf.Line(10, pdf.GetY(), 200, pdf.GetY())
- pdf.Ln(8)
-
- pdf.SetFont("Helvetica", "", 11)
-
- // Fixed sections first
- fixedEntries := []string{
- "Gefaehrdungsprotokoll",
- "Risikomatrix-Zusammenfassung",
- "Massnahmen-Uebersicht",
- }
-
- pageEstimate := 3 // Cover + TOC use pages 1-2, sections start at 3
- for i, section := range sections {
- pdf.CellFormat(10, 7, fmt.Sprintf("%d.", i+1), "", 0, "R", false, 0, "")
- pdf.CellFormat(5, 7, "", "", 0, "", false, 0, "")
- pdf.CellFormat(130, 7, section.Title, "", 0, "L", false, 0, "")
- pdf.CellFormat(0, 7, fmt.Sprintf("~%d", pageEstimate+i), "", 1, "R", false, 0, "")
- }
-
- // Append fixed sections after document sections
- startPage := pageEstimate + len(sections)
- for i, entry := range fixedEntries {
- idx := len(sections) + i + 1
- pdf.CellFormat(10, 7, fmt.Sprintf("%d.", idx), "", 0, "R", false, 0, "")
- pdf.CellFormat(5, 7, "", "", 0, "", false, 0, "")
- pdf.CellFormat(130, 7, entry, "", 0, "L", false, 0, "")
- pdf.CellFormat(0, 7, fmt.Sprintf("~%d", startPage+i), "", 1, "R", false, 0, "")
- }
-}
-
-func (e *DocumentExporter) pdfSection(pdf *gofpdf.Fpdf, section TechFileSection) {
- // Section heading
- pdf.SetFont("Helvetica", "B", 14)
- pdf.SetTextColor(50, 50, 50)
- pdf.CellFormat(0, 10, section.Title, "", 1, "L", false, 0, "")
- pdf.SetTextColor(0, 0, 0)
-
- // Status badge
- pdf.SetFont("Helvetica", "I", 9)
- pdf.SetTextColor(100, 100, 100)
- pdf.CellFormat(0, 5,
- fmt.Sprintf("Typ: %s | Status: %s | Version: %d",
- section.SectionType, string(section.Status), section.Version),
- "", 1, "L", false, 0, "")
- pdf.SetTextColor(0, 0, 0)
-
- pdf.SetDrawColor(200, 200, 200)
- pdf.Line(10, pdf.GetY(), 200, pdf.GetY())
- pdf.Ln(5)
-
- // Content
- pdf.SetFont("Helvetica", "", 10)
- if section.Content != "" {
- pdf.MultiCell(0, 5, section.Content, "", "L", false)
- } else {
- pdf.SetFont("Helvetica", "I", 10)
- pdf.SetTextColor(150, 150, 150)
- pdf.CellFormat(0, 7, "(Kein Inhalt vorhanden)", "", 1, "L", false, 0, "")
- pdf.SetTextColor(0, 0, 0)
- }
-}
-
-// buildAssessmentMap creates a lookup from HazardID to its most recent RiskAssessment
-func buildAssessmentMap(assessments []RiskAssessment) map[string]*RiskAssessment {
- m := make(map[string]*RiskAssessment)
- for i := range assessments {
- a := &assessments[i]
- key := a.HazardID.String()
- if existing, ok := m[key]; !ok || a.Version > existing.Version {
- m[key] = a
- }
- }
- return m
-}
-
-func (e *DocumentExporter) pdfHazardLog(pdf *gofpdf.Fpdf, hazards []Hazard, assessments []RiskAssessment) {
- pdf.SetFont("Helvetica", "B", 14)
- pdf.SetTextColor(50, 50, 50)
- pdf.CellFormat(0, 10, "Gefaehrdungsprotokoll", "", 1, "L", false, 0, "")
- pdf.SetTextColor(0, 0, 0)
- pdf.SetDrawColor(200, 200, 200)
- pdf.Line(10, pdf.GetY(), 200, pdf.GetY())
- pdf.Ln(5)
-
- if len(hazards) == 0 {
- pdf.SetFont("Helvetica", "I", 10)
- pdf.CellFormat(0, 7, "(Keine Gefaehrdungen erfasst)", "", 1, "L", false, 0, "")
- return
- }
-
- assessMap := buildAssessmentMap(assessments)
-
- // Table header
- colWidths := []float64{10, 40, 30, 12, 12, 12, 30, 20}
- headers := []string{"Nr", "Name", "Kategorie", "S", "E", "P", "Risiko", "OK"}
-
- pdf.SetFont("Helvetica", "B", 9)
- pdf.SetFillColor(240, 240, 240)
- for i, h := range headers {
- pdf.CellFormat(colWidths[i], 7, h, "1", 0, "C", true, 0, "")
- }
- pdf.Ln(-1)
-
- pdf.SetFont("Helvetica", "", 8)
- for i, hazard := range hazards {
- if pdf.GetY() > 265 {
- pdf.AddPage()
- // Reprint header
- pdf.SetFont("Helvetica", "B", 9)
- pdf.SetFillColor(240, 240, 240)
- for j, h := range headers {
- pdf.CellFormat(colWidths[j], 7, h, "1", 0, "C", true, 0, "")
- }
- pdf.Ln(-1)
- pdf.SetFont("Helvetica", "", 8)
- }
-
- a := assessMap[hazard.ID.String()]
-
- sev, exp, prob := "", "", ""
- riskLabel := "-"
- acceptable := "-"
- var rl RiskLevel
-
- if a != nil {
- sev = fmt.Sprintf("%d", a.Severity)
- exp = fmt.Sprintf("%d", a.Exposure)
- prob = fmt.Sprintf("%d", a.Probability)
- rl = a.RiskLevel
- riskLabel = riskLevelLabel(rl)
- if a.IsAcceptable {
- acceptable = "Ja"
- } else {
- acceptable = "Nein"
- }
- }
-
- // Color-code the row based on risk level
- r, g, b := riskLevelColor(rl)
- pdf.SetFillColor(r, g, b)
- fill := rl != ""
-
- pdf.CellFormat(colWidths[0], 6, fmt.Sprintf("%d", i+1), "1", 0, "C", fill, 0, "")
- pdf.CellFormat(colWidths[1], 6, pdfTruncate(hazard.Name, 22), "1", 0, "L", fill, 0, "")
- pdf.CellFormat(colWidths[2], 6, pdfTruncate(hazard.Category, 16), "1", 0, "L", fill, 0, "")
- pdf.CellFormat(colWidths[3], 6, sev, "1", 0, "C", fill, 0, "")
- pdf.CellFormat(colWidths[4], 6, exp, "1", 0, "C", fill, 0, "")
- pdf.CellFormat(colWidths[5], 6, prob, "1", 0, "C", fill, 0, "")
- pdf.CellFormat(colWidths[6], 6, riskLabel, "1", 0, "C", fill, 0, "")
- pdf.CellFormat(colWidths[7], 6, acceptable, "1", 0, "C", fill, 0, "")
- pdf.Ln(-1)
- }
-}
-
-func (e *DocumentExporter) pdfRiskMatrixSummary(pdf *gofpdf.Fpdf, assessments []RiskAssessment) {
- pdf.Ln(10)
- pdf.SetFont("Helvetica", "B", 14)
- pdf.SetTextColor(50, 50, 50)
- pdf.CellFormat(0, 10, "Risikomatrix-Zusammenfassung", "", 1, "L", false, 0, "")
- pdf.SetTextColor(0, 0, 0)
- pdf.SetDrawColor(200, 200, 200)
- pdf.Line(10, pdf.GetY(), 200, pdf.GetY())
- pdf.Ln(5)
-
- counts := countByRiskLevel(assessments)
-
- levels := []RiskLevel{
- RiskLevelNotAcceptable,
- RiskLevelVeryHigh,
- RiskLevelCritical,
- RiskLevelHigh,
- RiskLevelMedium,
- RiskLevelLow,
- RiskLevelNegligible,
- }
-
- pdf.SetFont("Helvetica", "B", 9)
- pdf.SetFillColor(240, 240, 240)
- pdf.CellFormat(60, 7, "Risikostufe", "1", 0, "L", true, 0, "")
- pdf.CellFormat(30, 7, "Anzahl", "1", 0, "C", true, 0, "")
- pdf.Ln(-1)
-
- pdf.SetFont("Helvetica", "", 9)
- for _, level := range levels {
- count := counts[level]
- if count == 0 {
- continue
- }
- r, g, b := riskLevelColor(level)
- pdf.SetFillColor(r, g, b)
- pdf.CellFormat(60, 6, riskLevelLabel(level), "1", 0, "L", true, 0, "")
- pdf.CellFormat(30, 6, fmt.Sprintf("%d", count), "1", 0, "C", true, 0, "")
- pdf.Ln(-1)
- }
-
- pdf.SetFont("Helvetica", "B", 9)
- pdf.SetFillColor(240, 240, 240)
- pdf.CellFormat(60, 7, "Gesamt", "1", 0, "L", true, 0, "")
- pdf.CellFormat(30, 7, fmt.Sprintf("%d", len(assessments)), "1", 0, "C", true, 0, "")
- pdf.Ln(-1)
-}
-
-func (e *DocumentExporter) pdfMitigationsTable(pdf *gofpdf.Fpdf, mitigations []Mitigation) {
- pdf.SetFont("Helvetica", "B", 14)
- pdf.SetTextColor(50, 50, 50)
- pdf.CellFormat(0, 10, "Massnahmen-Uebersicht", "", 1, "L", false, 0, "")
- pdf.SetTextColor(0, 0, 0)
- pdf.SetDrawColor(200, 200, 200)
- pdf.Line(10, pdf.GetY(), 200, pdf.GetY())
- pdf.Ln(5)
-
- if len(mitigations) == 0 {
- pdf.SetFont("Helvetica", "I", 10)
- pdf.CellFormat(0, 7, "(Keine Massnahmen erfasst)", "", 1, "L", false, 0, "")
- return
- }
-
- colWidths := []float64{10, 45, 30, 30, 40}
- headers := []string{"Nr", "Name", "Typ", "Status", "Verifikation"}
-
- pdf.SetFont("Helvetica", "B", 9)
- pdf.SetFillColor(240, 240, 240)
- for i, h := range headers {
- pdf.CellFormat(colWidths[i], 7, h, "1", 0, "C", true, 0, "")
- }
- pdf.Ln(-1)
-
- pdf.SetFont("Helvetica", "", 8)
- for i, m := range mitigations {
- if pdf.GetY() > 265 {
- pdf.AddPage()
- pdf.SetFont("Helvetica", "B", 9)
- pdf.SetFillColor(240, 240, 240)
- for j, h := range headers {
- pdf.CellFormat(colWidths[j], 7, h, "1", 0, "C", true, 0, "")
- }
- pdf.Ln(-1)
- pdf.SetFont("Helvetica", "", 8)
- }
-
- pdf.CellFormat(colWidths[0], 6, fmt.Sprintf("%d", i+1), "1", 0, "C", false, 0, "")
- pdf.CellFormat(colWidths[1], 6, pdfTruncate(m.Name, 25), "1", 0, "L", false, 0, "")
- pdf.CellFormat(colWidths[2], 6, reductionTypeLabel(m.ReductionType), "1", 0, "C", false, 0, "")
- pdf.CellFormat(colWidths[3], 6, mitigationStatusLabel(m.Status), "1", 0, "C", false, 0, "")
- pdf.CellFormat(colWidths[4], 6, pdfTruncate(string(m.VerificationMethod), 22), "1", 0, "L", false, 0, "")
- pdf.Ln(-1)
- }
-}
-
-func (e *DocumentExporter) pdfClassifications(pdf *gofpdf.Fpdf, classifications []RegulatoryClassification) {
- pdf.SetFont("Helvetica", "B", 14)
- pdf.SetTextColor(50, 50, 50)
- pdf.CellFormat(0, 10, "Regulatorische Klassifizierungen", "", 1, "L", false, 0, "")
- pdf.SetTextColor(0, 0, 0)
- pdf.SetDrawColor(200, 200, 200)
- pdf.Line(10, pdf.GetY(), 200, pdf.GetY())
- pdf.Ln(5)
-
- for _, c := range classifications {
- pdf.SetFont("Helvetica", "B", 11)
- pdf.CellFormat(0, 7, regulationLabel(c.Regulation), "", 1, "L", false, 0, "")
-
- pdf.SetFont("Helvetica", "", 10)
- pdf.CellFormat(50, 6, "Klassifizierung:", "", 0, "L", false, 0, "")
- pdf.CellFormat(0, 6, c.ClassificationResult, "", 1, "L", false, 0, "")
-
- pdf.CellFormat(50, 6, "Risikostufe:", "", 0, "L", false, 0, "")
- pdf.CellFormat(0, 6, riskLevelLabel(c.RiskLevel), "", 1, "L", false, 0, "")
-
- if c.Reasoning != "" {
- pdf.CellFormat(50, 6, "Begruendung:", "", 0, "L", false, 0, "")
- pdf.MultiCell(0, 5, c.Reasoning, "", "L", false)
- }
- pdf.Ln(5)
- }
-}
-
-// ============================================================================
-// Excel Export
-// ============================================================================
-
-// ExportExcel generates an XLSX workbook with project data across multiple sheets
-func (e *DocumentExporter) ExportExcel(
- project *Project,
- sections []TechFileSection,
- hazards []Hazard,
- assessments []RiskAssessment,
- mitigations []Mitigation,
-) ([]byte, error) {
- if project == nil {
- return nil, fmt.Errorf("project must not be nil")
- }
-
- f := excelize.NewFile()
- defer f.Close()
-
- // --- Sheet 1: Uebersicht ---
- overviewSheet := "Uebersicht"
- f.SetSheetName("Sheet1", overviewSheet)
- e.xlsxOverview(f, overviewSheet, project)
-
- // --- Sheet 2: Gefaehrdungsprotokoll ---
- hazardSheet := "Gefaehrdungsprotokoll"
- f.NewSheet(hazardSheet)
- e.xlsxHazardLog(f, hazardSheet, hazards, assessments)
-
- // --- Sheet 3: Massnahmen ---
- mitigationSheet := "Massnahmen"
- f.NewSheet(mitigationSheet)
- e.xlsxMitigations(f, mitigationSheet, mitigations)
-
- // --- Sheet 4: Risikomatrix ---
- matrixSheet := "Risikomatrix"
- f.NewSheet(matrixSheet)
- e.xlsxRiskMatrix(f, matrixSheet, assessments)
-
- // --- Sheet 5: Sektionen ---
- sectionSheet := "Sektionen"
- f.NewSheet(sectionSheet)
- e.xlsxSections(f, sectionSheet, sections)
-
- buf, err := f.WriteToBuffer()
- if err != nil {
- return nil, fmt.Errorf("failed to write Excel: %w", err)
- }
- return buf.Bytes(), nil
-}
-
-func (e *DocumentExporter) xlsxOverview(f *excelize.File, sheet string, project *Project) {
- headerStyle, _ := f.NewStyle(&excelize.Style{
- Font: &excelize.Font{Bold: true, Size: 11},
- Fill: excelize.Fill{Type: "pattern", Pattern: 1, Color: []string{"D9E1F2"}},
- })
-
- f.SetColWidth(sheet, "A", "A", 30)
- f.SetColWidth(sheet, "B", "B", 50)
-
- rows := [][]string{
- {"Eigenschaft", "Wert"},
- {"Maschinenname", project.MachineName},
- {"Maschinentyp", project.MachineType},
- {"Hersteller", project.Manufacturer},
- {"Beschreibung", project.Description},
- {"CE-Kennzeichnungsziel", project.CEMarkingTarget},
- {"Projektstatus", string(project.Status)},
- {"Vollstaendigkeits-Score", fmt.Sprintf("%.1f%%", project.CompletenessScore*100)},
- {"Erstellt am", project.CreatedAt.Format("02.01.2006 15:04")},
- {"Aktualisiert am", project.UpdatedAt.Format("02.01.2006 15:04")},
- }
-
- for i, row := range rows {
- rowNum := i + 1
- f.SetCellValue(sheet, cellRef("A", rowNum), row[0])
- f.SetCellValue(sheet, cellRef("B", rowNum), row[1])
- if i == 0 {
- f.SetCellStyle(sheet, cellRef("A", rowNum), cellRef("B", rowNum), headerStyle)
- }
- }
-}
-
-func (e *DocumentExporter) xlsxHazardLog(f *excelize.File, sheet string, hazards []Hazard, assessments []RiskAssessment) {
- headerStyle, _ := f.NewStyle(&excelize.Style{
- Font: &excelize.Font{Bold: true, Size: 10, Color: "FFFFFF"},
- Fill: excelize.Fill{Type: "pattern", Pattern: 1, Color: []string{"4472C4"}},
- Alignment: &excelize.Alignment{Horizontal: "center"},
- })
-
- headers := []string{"Nr", "Name", "Kategorie", "Beschreibung", "S", "E", "P", "A",
- "Inherent Risk", "C_eff", "Residual Risk", "Risk Level", "Akzeptabel"}
-
- colWidths := map[string]float64{
- "A": 6, "B": 25, "C": 20, "D": 35, "E": 8, "F": 8, "G": 8, "H": 8,
- "I": 14, "J": 10, "K": 14, "L": 18, "M": 12,
- }
- for col, w := range colWidths {
- f.SetColWidth(sheet, col, col, w)
- }
-
- cols := []string{"A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M"}
- for i, h := range headers {
- f.SetCellValue(sheet, cellRef(cols[i], 1), h)
- }
- f.SetCellStyle(sheet, "A1", cellRef(cols[len(cols)-1], 1), headerStyle)
-
- assessMap := buildAssessmentMap(assessments)
-
- for i, hazard := range hazards {
- row := i + 2
- a := assessMap[hazard.ID.String()]
-
- f.SetCellValue(sheet, cellRef("A", row), i+1)
- f.SetCellValue(sheet, cellRef("B", row), hazard.Name)
- f.SetCellValue(sheet, cellRef("C", row), hazard.Category)
- f.SetCellValue(sheet, cellRef("D", row), hazard.Description)
-
- if a != nil {
- f.SetCellValue(sheet, cellRef("E", row), a.Severity)
- f.SetCellValue(sheet, cellRef("F", row), a.Exposure)
- f.SetCellValue(sheet, cellRef("G", row), a.Probability)
- f.SetCellValue(sheet, cellRef("H", row), a.Avoidance)
- f.SetCellValue(sheet, cellRef("I", row), fmt.Sprintf("%.1f", a.InherentRisk))
- f.SetCellValue(sheet, cellRef("J", row), fmt.Sprintf("%.2f", a.CEff))
- f.SetCellValue(sheet, cellRef("K", row), fmt.Sprintf("%.1f", a.ResidualRisk))
- f.SetCellValue(sheet, cellRef("L", row), riskLevelLabel(a.RiskLevel))
-
- acceptStr := "Nein"
- if a.IsAcceptable {
- acceptStr = "Ja"
- }
- f.SetCellValue(sheet, cellRef("M", row), acceptStr)
-
- // Color-code the risk level cell
- r, g, b := riskLevelColor(a.RiskLevel)
- style, _ := f.NewStyle(&excelize.Style{
- Fill: excelize.Fill{
- Type: "pattern",
- Pattern: 1,
- Color: []string{rgbHex(r, g, b)},
- },
- Alignment: &excelize.Alignment{Horizontal: "center"},
- })
- f.SetCellStyle(sheet, cellRef("L", row), cellRef("L", row), style)
- }
- }
-}
-
-func (e *DocumentExporter) xlsxMitigations(f *excelize.File, sheet string, mitigations []Mitigation) {
- headerStyle, _ := f.NewStyle(&excelize.Style{
- Font: &excelize.Font{Bold: true, Size: 10, Color: "FFFFFF"},
- Fill: excelize.Fill{Type: "pattern", Pattern: 1, Color: []string{"4472C4"}},
- Alignment: &excelize.Alignment{Horizontal: "center"},
- })
-
- headers := []string{"Nr", "Name", "Typ", "Beschreibung", "Status", "Verifikationsmethode", "Ergebnis"}
- cols := []string{"A", "B", "C", "D", "E", "F", "G"}
-
- f.SetColWidth(sheet, "A", "A", 6)
- f.SetColWidth(sheet, "B", "B", 25)
- f.SetColWidth(sheet, "C", "C", 15)
- f.SetColWidth(sheet, "D", "D", 35)
- f.SetColWidth(sheet, "E", "E", 15)
- f.SetColWidth(sheet, "F", "F", 22)
- f.SetColWidth(sheet, "G", "G", 25)
-
- for i, h := range headers {
- f.SetCellValue(sheet, cellRef(cols[i], 1), h)
- }
- f.SetCellStyle(sheet, "A1", cellRef(cols[len(cols)-1], 1), headerStyle)
-
- for i, m := range mitigations {
- row := i + 2
- f.SetCellValue(sheet, cellRef("A", row), i+1)
- f.SetCellValue(sheet, cellRef("B", row), m.Name)
- f.SetCellValue(sheet, cellRef("C", row), reductionTypeLabel(m.ReductionType))
- f.SetCellValue(sheet, cellRef("D", row), m.Description)
- f.SetCellValue(sheet, cellRef("E", row), mitigationStatusLabel(m.Status))
- f.SetCellValue(sheet, cellRef("F", row), string(m.VerificationMethod))
- f.SetCellValue(sheet, cellRef("G", row), m.VerificationResult)
- }
-}
-
-func (e *DocumentExporter) xlsxRiskMatrix(f *excelize.File, sheet string, assessments []RiskAssessment) {
- headerStyle, _ := f.NewStyle(&excelize.Style{
- Font: &excelize.Font{Bold: true, Size: 10, Color: "FFFFFF"},
- Fill: excelize.Fill{Type: "pattern", Pattern: 1, Color: []string{"4472C4"}},
- Alignment: &excelize.Alignment{Horizontal: "center"},
- })
-
- f.SetColWidth(sheet, "A", "A", 25)
- f.SetColWidth(sheet, "B", "B", 12)
-
- f.SetCellValue(sheet, "A1", "Risikostufe")
- f.SetCellValue(sheet, "B1", "Anzahl")
- f.SetCellStyle(sheet, "A1", "B1", headerStyle)
-
- counts := countByRiskLevel(assessments)
-
- levels := []RiskLevel{
- RiskLevelNotAcceptable,
- RiskLevelVeryHigh,
- RiskLevelCritical,
- RiskLevelHigh,
- RiskLevelMedium,
- RiskLevelLow,
- RiskLevelNegligible,
- }
-
- row := 2
- for _, level := range levels {
- count := counts[level]
- f.SetCellValue(sheet, cellRef("A", row), riskLevelLabel(level))
- f.SetCellValue(sheet, cellRef("B", row), count)
-
- r, g, b := riskLevelColor(level)
- style, _ := f.NewStyle(&excelize.Style{
- Fill: excelize.Fill{
- Type: "pattern",
- Pattern: 1,
- Color: []string{rgbHex(r, g, b)},
- },
- })
- f.SetCellStyle(sheet, cellRef("A", row), cellRef("B", row), style)
- row++
- }
-
- // Total row
- totalStyle, _ := f.NewStyle(&excelize.Style{
- Font: &excelize.Font{Bold: true},
- Fill: excelize.Fill{Type: "pattern", Pattern: 1, Color: []string{"D9E1F2"}},
- })
- f.SetCellValue(sheet, cellRef("A", row), "Gesamt")
- f.SetCellValue(sheet, cellRef("B", row), len(assessments))
- f.SetCellStyle(sheet, cellRef("A", row), cellRef("B", row), totalStyle)
-}
-
-func (e *DocumentExporter) xlsxSections(f *excelize.File, sheet string, sections []TechFileSection) {
- headerStyle, _ := f.NewStyle(&excelize.Style{
- Font: &excelize.Font{Bold: true, Size: 10, Color: "FFFFFF"},
- Fill: excelize.Fill{Type: "pattern", Pattern: 1, Color: []string{"4472C4"}},
- Alignment: &excelize.Alignment{Horizontal: "center"},
- })
-
- f.SetColWidth(sheet, "A", "A", 25)
- f.SetColWidth(sheet, "B", "B", 40)
- f.SetColWidth(sheet, "C", "C", 15)
-
- headers := []string{"Sektion", "Titel", "Status"}
- cols := []string{"A", "B", "C"}
-
- for i, h := range headers {
- f.SetCellValue(sheet, cellRef(cols[i], 1), h)
- }
- f.SetCellStyle(sheet, "A1", cellRef(cols[len(cols)-1], 1), headerStyle)
-
- for i, s := range sections {
- row := i + 2
- f.SetCellValue(sheet, cellRef("A", row), s.SectionType)
- f.SetCellValue(sheet, cellRef("B", row), s.Title)
- f.SetCellValue(sheet, cellRef("C", row), string(s.Status))
- }
-}
-
// ============================================================================
// Markdown Export
// ============================================================================
@@ -709,10 +110,8 @@ func (e *DocumentExporter) ExportMarkdown(
var buf bytes.Buffer
- // Title
buf.WriteString(fmt.Sprintf("# CE-Akte: %s\n\n", project.MachineName))
- // Metadata block
buf.WriteString("| Eigenschaft | Wert |\n")
buf.WriteString("|-------------|------|\n")
buf.WriteString(fmt.Sprintf("| Hersteller | %s |\n", project.Manufacturer))
@@ -728,7 +127,6 @@ func (e *DocumentExporter) ExportMarkdown(
buf.WriteString(fmt.Sprintf("> %s\n\n", project.Description))
}
- // Sections
for _, section := range sections {
buf.WriteString(fmt.Sprintf("## %s\n\n", section.Title))
buf.WriteString(fmt.Sprintf("*Typ: %s | Status: %s | Version: %d*\n\n",
@@ -741,361 +139,9 @@ func (e *DocumentExporter) ExportMarkdown(
}
}
- // Footer
buf.WriteString("---\n\n")
buf.WriteString(fmt.Sprintf("*Generiert am %s mit BreakPilot AI Compliance SDK*\n",
time.Now().Format("02.01.2006 15:04")))
return buf.Bytes(), nil
}
-
-// ============================================================================
-// DOCX Export (minimal OOXML via archive/zip)
-// ============================================================================
-
-// ExportDOCX generates a minimal DOCX file containing the CE technical file sections
-func (e *DocumentExporter) ExportDOCX(
- project *Project,
- sections []TechFileSection,
-) ([]byte, error) {
- if project == nil {
- return nil, fmt.Errorf("project must not be nil")
- }
-
- var buf bytes.Buffer
- zw := zip.NewWriter(&buf)
-
- // [Content_Types].xml
- contentTypes := `
-
-
-
-
-`
- if err := addZipEntry(zw, "[Content_Types].xml", contentTypes); err != nil {
- return nil, fmt.Errorf("failed to write [Content_Types].xml: %w", err)
- }
-
- // _rels/.rels
- rels := `
-
-
-`
- if err := addZipEntry(zw, "_rels/.rels", rels); err != nil {
- return nil, fmt.Errorf("failed to write _rels/.rels: %w", err)
- }
-
- // word/_rels/document.xml.rels
- docRels := `
-
-`
- if err := addZipEntry(zw, "word/_rels/document.xml.rels", docRels); err != nil {
- return nil, fmt.Errorf("failed to write word/_rels/document.xml.rels: %w", err)
- }
-
- // word/document.xml — build the body
- docXML := e.buildDocumentXML(project, sections)
- if err := addZipEntry(zw, "word/document.xml", docXML); err != nil {
- return nil, fmt.Errorf("failed to write word/document.xml: %w", err)
- }
-
- if err := zw.Close(); err != nil {
- return nil, fmt.Errorf("failed to close ZIP: %w", err)
- }
-
- return buf.Bytes(), nil
-}
-
-func (e *DocumentExporter) buildDocumentXML(project *Project, sections []TechFileSection) string {
- var body strings.Builder
-
- // Title paragraph (Heading 1 style)
- body.WriteString(docxHeading(fmt.Sprintf("CE-Akte: %s", project.MachineName), 1))
-
- // Metadata paragraphs
- metaLines := []string{
- fmt.Sprintf("Hersteller: %s", project.Manufacturer),
- fmt.Sprintf("Maschinentyp: %s", project.MachineType),
- }
- if project.CEMarkingTarget != "" {
- metaLines = append(metaLines, fmt.Sprintf("CE-Kennzeichnungsziel: %s", project.CEMarkingTarget))
- }
- metaLines = append(metaLines,
- fmt.Sprintf("Status: %s", project.Status),
- fmt.Sprintf("Datum: %s", time.Now().Format("02.01.2006")),
- )
- for _, line := range metaLines {
- body.WriteString(docxParagraph(line, false))
- }
-
- if project.Description != "" {
- body.WriteString(docxParagraph("", false)) // blank line
- body.WriteString(docxParagraph(project.Description, true))
- }
-
- // Sections
- for _, section := range sections {
- body.WriteString(docxHeading(section.Title, 2))
- body.WriteString(docxParagraph(
- fmt.Sprintf("Typ: %s | Status: %s | Version: %d",
- section.SectionType, string(section.Status), section.Version),
- true,
- ))
-
- if section.Content != "" {
- // Split content by newlines into separate paragraphs
- for _, line := range strings.Split(section.Content, "\n") {
- body.WriteString(docxParagraph(line, false))
- }
- } else {
- body.WriteString(docxParagraph("(Kein Inhalt vorhanden)", true))
- }
- }
-
- // Footer
- body.WriteString(docxParagraph("", false))
- body.WriteString(docxParagraph(
- fmt.Sprintf("Generiert am %s mit BreakPilot AI Compliance SDK",
- time.Now().Format("02.01.2006 15:04")),
- true,
- ))
-
- return fmt.Sprintf(`
-
-
-%s
-
-`, body.String())
-}
-
-// ============================================================================
-// JSON Export (convenience — returns marshalled project data)
-// ============================================================================
-
-// ExportJSON returns a JSON representation of the project export data
-func (e *DocumentExporter) ExportJSON(
- project *Project,
- sections []TechFileSection,
- hazards []Hazard,
- assessments []RiskAssessment,
- mitigations []Mitigation,
- classifications []RegulatoryClassification,
-) ([]byte, error) {
- if project == nil {
- return nil, fmt.Errorf("project must not be nil")
- }
-
- payload := map[string]interface{}{
- "project": project,
- "sections": sections,
- "hazards": hazards,
- "assessments": assessments,
- "mitigations": mitigations,
- "classifications": classifications,
- "exported_at": time.Now().UTC().Format(time.RFC3339),
- "format_version": "1.0",
- }
-
- data, err := json.MarshalIndent(payload, "", " ")
- if err != nil {
- return nil, fmt.Errorf("failed to marshal JSON: %w", err)
- }
- return data, nil
-}
-
-// ============================================================================
-// Helper Functions
-// ============================================================================
-
-// riskLevelColor returns RGB values for color-coding a given risk level
-func riskLevelColor(level RiskLevel) (r, g, b int) {
- switch level {
- case RiskLevelNotAcceptable:
- return 180, 0, 0 // dark red
- case RiskLevelVeryHigh:
- return 220, 40, 40 // red
- case RiskLevelCritical:
- return 255, 80, 80 // bright red
- case RiskLevelHigh:
- return 255, 165, 80 // orange
- case RiskLevelMedium:
- return 255, 230, 100 // yellow
- case RiskLevelLow:
- return 180, 230, 140 // light green
- case RiskLevelNegligible:
- return 140, 210, 140 // green
- default:
- return 240, 240, 240 // light gray (unassessed)
- }
-}
-
-// riskLevelLabel returns a German display label for a risk level
-func riskLevelLabel(level RiskLevel) string {
- switch level {
- case RiskLevelNotAcceptable:
- return "Nicht akzeptabel"
- case RiskLevelVeryHigh:
- return "Sehr hoch"
- case RiskLevelCritical:
- return "Kritisch"
- case RiskLevelHigh:
- return "Hoch"
- case RiskLevelMedium:
- return "Mittel"
- case RiskLevelLow:
- return "Niedrig"
- case RiskLevelNegligible:
- return "Vernachlaessigbar"
- default:
- return string(level)
- }
-}
-
-// reductionTypeLabel returns a German label for a reduction type
-func reductionTypeLabel(rt ReductionType) string {
- switch rt {
- case ReductionTypeDesign:
- return "Konstruktiv"
- case ReductionTypeProtective:
- return "Schutzmassnahme"
- case ReductionTypeInformation:
- return "Information"
- default:
- return string(rt)
- }
-}
-
-// mitigationStatusLabel returns a German label for a mitigation status
-func mitigationStatusLabel(status MitigationStatus) string {
- switch status {
- case MitigationStatusPlanned:
- return "Geplant"
- case MitigationStatusImplemented:
- return "Umgesetzt"
- case MitigationStatusVerified:
- return "Verifiziert"
- case MitigationStatusRejected:
- return "Abgelehnt"
- default:
- return string(status)
- }
-}
-
-// regulationLabel returns a German label for a regulation type
-func regulationLabel(reg RegulationType) string {
- switch reg {
- case RegulationNIS2:
- return "NIS-2 Richtlinie"
- case RegulationAIAct:
- return "EU AI Act"
- case RegulationCRA:
- return "Cyber Resilience Act"
- case RegulationMachineryRegulation:
- return "EU Maschinenverordnung 2023/1230"
- default:
- return string(reg)
- }
-}
-
-// escapeXML escapes special XML characters in text content
-func escapeXML(s string) string {
- var buf bytes.Buffer
- if err := xml.EscapeText(&buf, []byte(s)); err != nil {
- // Fallback: return input unchanged (xml.EscapeText should never error on valid UTF-8)
- return s
- }
- return buf.String()
-}
-
-// countByRiskLevel counts assessments per risk level
-func countByRiskLevel(assessments []RiskAssessment) map[RiskLevel]int {
- counts := make(map[RiskLevel]int)
- for _, a := range assessments {
- counts[a.RiskLevel]++
- }
- return counts
-}
-
-// pdfTruncate truncates a string for PDF cell display
-func pdfTruncate(s string, maxLen int) string {
- runes := []rune(s)
- if len(runes) <= maxLen {
- return s
- }
- if maxLen <= 3 {
- return string(runes[:maxLen])
- }
- return string(runes[:maxLen-3]) + "..."
-}
-
-// cellRef builds an Excel cell reference like "A1", "B12"
-func cellRef(col string, row int) string {
- return fmt.Sprintf("%s%d", col, row)
-}
-
-// rgbHex converts RGB values to a hex color string (without #)
-func rgbHex(r, g, b int) string {
- return fmt.Sprintf("%02X%02X%02X", r, g, b)
-}
-
-// addZipEntry writes a text file into a zip archive
-func addZipEntry(zw *zip.Writer, name, content string) error {
- w, err := zw.Create(name)
- if err != nil {
- return err
- }
- _, err = w.Write([]byte(content))
- return err
-}
-
-// docxHeading builds a DOCX paragraph with a heading style
-func docxHeading(text string, level int) string {
- // Map level to font size (in half-points): 1→32pt, 2→26pt, 3→22pt
- sizes := map[int]int{1: 64, 2: 52, 3: 44}
- sz, ok := sizes[level]
- if !ok {
- sz = 44
- }
- escaped := escapeXML(text)
- return fmt.Sprintf(`
-
-
-
-
-
-
- %s
-
-
-`, level, sz, sz, escaped)
-}
-
-// docxParagraph builds a DOCX paragraph, optionally italic
-func docxParagraph(text string, italic bool) string {
- escaped := escapeXML(text)
- rpr := ""
- if italic {
- rpr = ""
- }
- return fmt.Sprintf(`
-
- %s
- %s
-
-
-`, rpr, escaped)
-}
diff --git a/ai-compliance-sdk/internal/iace/document_export_docx_json.go b/ai-compliance-sdk/internal/iace/document_export_docx_json.go
new file mode 100644
index 0000000..926b518
--- /dev/null
+++ b/ai-compliance-sdk/internal/iace/document_export_docx_json.go
@@ -0,0 +1,161 @@
+package iace
+
+import (
+ "archive/zip"
+ "bytes"
+ "encoding/json"
+ "fmt"
+ "strings"
+ "time"
+)
+
+// ExportDOCX generates a minimal DOCX file containing the CE technical file sections
+func (e *DocumentExporter) ExportDOCX(
+ project *Project,
+ sections []TechFileSection,
+) ([]byte, error) {
+ if project == nil {
+ return nil, fmt.Errorf("project must not be nil")
+ }
+
+ var buf bytes.Buffer
+ zw := zip.NewWriter(&buf)
+
+ contentTypes := `
+
+
+
+
+`
+ if err := addZipEntry(zw, "[Content_Types].xml", contentTypes); err != nil {
+ return nil, fmt.Errorf("failed to write [Content_Types].xml: %w", err)
+ }
+
+ rels := `
+
+
+`
+ if err := addZipEntry(zw, "_rels/.rels", rels); err != nil {
+ return nil, fmt.Errorf("failed to write _rels/.rels: %w", err)
+ }
+
+ docRels := `
+
+`
+ if err := addZipEntry(zw, "word/_rels/document.xml.rels", docRels); err != nil {
+ return nil, fmt.Errorf("failed to write word/_rels/document.xml.rels: %w", err)
+ }
+
+ docXML := e.buildDocumentXML(project, sections)
+ if err := addZipEntry(zw, "word/document.xml", docXML); err != nil {
+ return nil, fmt.Errorf("failed to write word/document.xml: %w", err)
+ }
+
+ if err := zw.Close(); err != nil {
+ return nil, fmt.Errorf("failed to close ZIP: %w", err)
+ }
+
+ return buf.Bytes(), nil
+}
+
+func (e *DocumentExporter) buildDocumentXML(project *Project, sections []TechFileSection) string {
+ var body strings.Builder
+
+ body.WriteString(docxHeading(fmt.Sprintf("CE-Akte: %s", project.MachineName), 1))
+
+ metaLines := []string{
+ fmt.Sprintf("Hersteller: %s", project.Manufacturer),
+ fmt.Sprintf("Maschinentyp: %s", project.MachineType),
+ }
+ if project.CEMarkingTarget != "" {
+ metaLines = append(metaLines, fmt.Sprintf("CE-Kennzeichnungsziel: %s", project.CEMarkingTarget))
+ }
+ metaLines = append(metaLines,
+ fmt.Sprintf("Status: %s", project.Status),
+ fmt.Sprintf("Datum: %s", time.Now().Format("02.01.2006")),
+ )
+ for _, line := range metaLines {
+ body.WriteString(docxParagraph(line, false))
+ }
+
+ if project.Description != "" {
+ body.WriteString(docxParagraph("", false))
+ body.WriteString(docxParagraph(project.Description, true))
+ }
+
+ for _, section := range sections {
+ body.WriteString(docxHeading(section.Title, 2))
+ body.WriteString(docxParagraph(
+ fmt.Sprintf("Typ: %s | Status: %s | Version: %d",
+ section.SectionType, string(section.Status), section.Version),
+ true,
+ ))
+
+ if section.Content != "" {
+ for _, line := range strings.Split(section.Content, "\n") {
+ body.WriteString(docxParagraph(line, false))
+ }
+ } else {
+ body.WriteString(docxParagraph("(Kein Inhalt vorhanden)", true))
+ }
+ }
+
+ body.WriteString(docxParagraph("", false))
+ body.WriteString(docxParagraph(
+ fmt.Sprintf("Generiert am %s mit BreakPilot AI Compliance SDK",
+ time.Now().Format("02.01.2006 15:04")),
+ true,
+ ))
+
+ return fmt.Sprintf(`
+
+
+%s
+
+`, body.String())
+}
+
+// ExportJSON returns a JSON representation of the project export data
+func (e *DocumentExporter) ExportJSON(
+ project *Project,
+ sections []TechFileSection,
+ hazards []Hazard,
+ assessments []RiskAssessment,
+ mitigations []Mitigation,
+ classifications []RegulatoryClassification,
+) ([]byte, error) {
+ if project == nil {
+ return nil, fmt.Errorf("project must not be nil")
+ }
+
+ payload := map[string]interface{}{
+ "project": project,
+ "sections": sections,
+ "hazards": hazards,
+ "assessments": assessments,
+ "mitigations": mitigations,
+ "classifications": classifications,
+ "exported_at": time.Now().UTC().Format(time.RFC3339),
+ "format_version": "1.0",
+ }
+
+ data, err := json.MarshalIndent(payload, "", " ")
+ if err != nil {
+ return nil, fmt.Errorf("failed to marshal JSON: %w", err)
+ }
+ return data, nil
+}
diff --git a/ai-compliance-sdk/internal/iace/document_export_excel.go b/ai-compliance-sdk/internal/iace/document_export_excel.go
new file mode 100644
index 0000000..fb1d38c
--- /dev/null
+++ b/ai-compliance-sdk/internal/iace/document_export_excel.go
@@ -0,0 +1,261 @@
+package iace
+
+import (
+ "fmt"
+
+ "github.com/xuri/excelize/v2"
+)
+
+// ExportExcel generates an XLSX workbook with project data across multiple sheets
+func (e *DocumentExporter) ExportExcel(
+ project *Project,
+ sections []TechFileSection,
+ hazards []Hazard,
+ assessments []RiskAssessment,
+ mitigations []Mitigation,
+) ([]byte, error) {
+ if project == nil {
+ return nil, fmt.Errorf("project must not be nil")
+ }
+
+ f := excelize.NewFile()
+ defer f.Close()
+
+ overviewSheet := "Uebersicht"
+ f.SetSheetName("Sheet1", overviewSheet)
+ e.xlsxOverview(f, overviewSheet, project)
+
+ hazardSheet := "Gefaehrdungsprotokoll"
+ f.NewSheet(hazardSheet)
+ e.xlsxHazardLog(f, hazardSheet, hazards, assessments)
+
+ mitigationSheet := "Massnahmen"
+ f.NewSheet(mitigationSheet)
+ e.xlsxMitigations(f, mitigationSheet, mitigations)
+
+ matrixSheet := "Risikomatrix"
+ f.NewSheet(matrixSheet)
+ e.xlsxRiskMatrix(f, matrixSheet, assessments)
+
+ sectionSheet := "Sektionen"
+ f.NewSheet(sectionSheet)
+ e.xlsxSections(f, sectionSheet, sections)
+
+ buf, err := f.WriteToBuffer()
+ if err != nil {
+ return nil, fmt.Errorf("failed to write Excel: %w", err)
+ }
+ return buf.Bytes(), nil
+}
+
+func (e *DocumentExporter) xlsxOverview(f *excelize.File, sheet string, project *Project) {
+ headerStyle, _ := f.NewStyle(&excelize.Style{
+ Font: &excelize.Font{Bold: true, Size: 11},
+ Fill: excelize.Fill{Type: "pattern", Pattern: 1, Color: []string{"D9E1F2"}},
+ })
+
+ f.SetColWidth(sheet, "A", "A", 30)
+ f.SetColWidth(sheet, "B", "B", 50)
+
+ rows := [][]string{
+ {"Eigenschaft", "Wert"},
+ {"Maschinenname", project.MachineName},
+ {"Maschinentyp", project.MachineType},
+ {"Hersteller", project.Manufacturer},
+ {"Beschreibung", project.Description},
+ {"CE-Kennzeichnungsziel", project.CEMarkingTarget},
+ {"Projektstatus", string(project.Status)},
+ {"Vollstaendigkeits-Score", fmt.Sprintf("%.1f%%", project.CompletenessScore*100)},
+ {"Erstellt am", project.CreatedAt.Format("02.01.2006 15:04")},
+ {"Aktualisiert am", project.UpdatedAt.Format("02.01.2006 15:04")},
+ }
+
+ for i, row := range rows {
+ rowNum := i + 1
+ f.SetCellValue(sheet, cellRef("A", rowNum), row[0])
+ f.SetCellValue(sheet, cellRef("B", rowNum), row[1])
+ if i == 0 {
+ f.SetCellStyle(sheet, cellRef("A", rowNum), cellRef("B", rowNum), headerStyle)
+ }
+ }
+}
+
+func (e *DocumentExporter) xlsxHazardLog(f *excelize.File, sheet string, hazards []Hazard, assessments []RiskAssessment) {
+ headerStyle, _ := f.NewStyle(&excelize.Style{
+ Font: &excelize.Font{Bold: true, Size: 10, Color: "FFFFFF"},
+ Fill: excelize.Fill{Type: "pattern", Pattern: 1, Color: []string{"4472C4"}},
+ Alignment: &excelize.Alignment{Horizontal: "center"},
+ })
+
+ headers := []string{"Nr", "Name", "Kategorie", "Beschreibung", "S", "E", "P", "A",
+ "Inherent Risk", "C_eff", "Residual Risk", "Risk Level", "Akzeptabel"}
+
+ colWidths := map[string]float64{
+ "A": 6, "B": 25, "C": 20, "D": 35, "E": 8, "F": 8, "G": 8, "H": 8,
+ "I": 14, "J": 10, "K": 14, "L": 18, "M": 12,
+ }
+ for col, w := range colWidths {
+ f.SetColWidth(sheet, col, col, w)
+ }
+
+ cols := []string{"A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M"}
+ for i, h := range headers {
+ f.SetCellValue(sheet, cellRef(cols[i], 1), h)
+ }
+ f.SetCellStyle(sheet, "A1", cellRef(cols[len(cols)-1], 1), headerStyle)
+
+ assessMap := buildAssessmentMap(assessments)
+
+ for i, hazard := range hazards {
+ row := i + 2
+ a := assessMap[hazard.ID.String()]
+
+ f.SetCellValue(sheet, cellRef("A", row), i+1)
+ f.SetCellValue(sheet, cellRef("B", row), hazard.Name)
+ f.SetCellValue(sheet, cellRef("C", row), hazard.Category)
+ f.SetCellValue(sheet, cellRef("D", row), hazard.Description)
+
+ if a != nil {
+ f.SetCellValue(sheet, cellRef("E", row), a.Severity)
+ f.SetCellValue(sheet, cellRef("F", row), a.Exposure)
+ f.SetCellValue(sheet, cellRef("G", row), a.Probability)
+ f.SetCellValue(sheet, cellRef("H", row), a.Avoidance)
+ f.SetCellValue(sheet, cellRef("I", row), fmt.Sprintf("%.1f", a.InherentRisk))
+ f.SetCellValue(sheet, cellRef("J", row), fmt.Sprintf("%.2f", a.CEff))
+ f.SetCellValue(sheet, cellRef("K", row), fmt.Sprintf("%.1f", a.ResidualRisk))
+ f.SetCellValue(sheet, cellRef("L", row), riskLevelLabel(a.RiskLevel))
+
+ acceptStr := "Nein"
+ if a.IsAcceptable {
+ acceptStr = "Ja"
+ }
+ f.SetCellValue(sheet, cellRef("M", row), acceptStr)
+
+ r, g, b := riskLevelColor(a.RiskLevel)
+ style, _ := f.NewStyle(&excelize.Style{
+ Fill: excelize.Fill{
+ Type: "pattern",
+ Pattern: 1,
+ Color: []string{rgbHex(r, g, b)},
+ },
+ Alignment: &excelize.Alignment{Horizontal: "center"},
+ })
+ f.SetCellStyle(sheet, cellRef("L", row), cellRef("L", row), style)
+ }
+ }
+}
+
+func (e *DocumentExporter) xlsxMitigations(f *excelize.File, sheet string, mitigations []Mitigation) {
+ headerStyle, _ := f.NewStyle(&excelize.Style{
+ Font: &excelize.Font{Bold: true, Size: 10, Color: "FFFFFF"},
+ Fill: excelize.Fill{Type: "pattern", Pattern: 1, Color: []string{"4472C4"}},
+ Alignment: &excelize.Alignment{Horizontal: "center"},
+ })
+
+ headers := []string{"Nr", "Name", "Typ", "Beschreibung", "Status", "Verifikationsmethode", "Ergebnis"}
+ cols := []string{"A", "B", "C", "D", "E", "F", "G"}
+
+ f.SetColWidth(sheet, "A", "A", 6)
+ f.SetColWidth(sheet, "B", "B", 25)
+ f.SetColWidth(sheet, "C", "C", 15)
+ f.SetColWidth(sheet, "D", "D", 35)
+ f.SetColWidth(sheet, "E", "E", 15)
+ f.SetColWidth(sheet, "F", "F", 22)
+ f.SetColWidth(sheet, "G", "G", 25)
+
+ for i, h := range headers {
+ f.SetCellValue(sheet, cellRef(cols[i], 1), h)
+ }
+ f.SetCellStyle(sheet, "A1", cellRef(cols[len(cols)-1], 1), headerStyle)
+
+ for i, m := range mitigations {
+ row := i + 2
+ f.SetCellValue(sheet, cellRef("A", row), i+1)
+ f.SetCellValue(sheet, cellRef("B", row), m.Name)
+ f.SetCellValue(sheet, cellRef("C", row), reductionTypeLabel(m.ReductionType))
+ f.SetCellValue(sheet, cellRef("D", row), m.Description)
+ f.SetCellValue(sheet, cellRef("E", row), mitigationStatusLabel(m.Status))
+ f.SetCellValue(sheet, cellRef("F", row), string(m.VerificationMethod))
+ f.SetCellValue(sheet, cellRef("G", row), m.VerificationResult)
+ }
+}
+
+func (e *DocumentExporter) xlsxRiskMatrix(f *excelize.File, sheet string, assessments []RiskAssessment) {
+ headerStyle, _ := f.NewStyle(&excelize.Style{
+ Font: &excelize.Font{Bold: true, Size: 10, Color: "FFFFFF"},
+ Fill: excelize.Fill{Type: "pattern", Pattern: 1, Color: []string{"4472C4"}},
+ Alignment: &excelize.Alignment{Horizontal: "center"},
+ })
+
+ f.SetColWidth(sheet, "A", "A", 25)
+ f.SetColWidth(sheet, "B", "B", 12)
+
+ f.SetCellValue(sheet, "A1", "Risikostufe")
+ f.SetCellValue(sheet, "B1", "Anzahl")
+ f.SetCellStyle(sheet, "A1", "B1", headerStyle)
+
+ counts := countByRiskLevel(assessments)
+
+ levels := []RiskLevel{
+ RiskLevelNotAcceptable,
+ RiskLevelVeryHigh,
+ RiskLevelCritical,
+ RiskLevelHigh,
+ RiskLevelMedium,
+ RiskLevelLow,
+ RiskLevelNegligible,
+ }
+
+ row := 2
+ for _, level := range levels {
+ count := counts[level]
+ f.SetCellValue(sheet, cellRef("A", row), riskLevelLabel(level))
+ f.SetCellValue(sheet, cellRef("B", row), count)
+
+ r, g, b := riskLevelColor(level)
+ style, _ := f.NewStyle(&excelize.Style{
+ Fill: excelize.Fill{
+ Type: "pattern",
+ Pattern: 1,
+ Color: []string{rgbHex(r, g, b)},
+ },
+ })
+ f.SetCellStyle(sheet, cellRef("A", row), cellRef("B", row), style)
+ row++
+ }
+
+ totalStyle, _ := f.NewStyle(&excelize.Style{
+ Font: &excelize.Font{Bold: true},
+ Fill: excelize.Fill{Type: "pattern", Pattern: 1, Color: []string{"D9E1F2"}},
+ })
+ f.SetCellValue(sheet, cellRef("A", row), "Gesamt")
+ f.SetCellValue(sheet, cellRef("B", row), len(assessments))
+ f.SetCellStyle(sheet, cellRef("A", row), cellRef("B", row), totalStyle)
+}
+
+func (e *DocumentExporter) xlsxSections(f *excelize.File, sheet string, sections []TechFileSection) {
+ headerStyle, _ := f.NewStyle(&excelize.Style{
+ Font: &excelize.Font{Bold: true, Size: 10, Color: "FFFFFF"},
+ Fill: excelize.Fill{Type: "pattern", Pattern: 1, Color: []string{"4472C4"}},
+ Alignment: &excelize.Alignment{Horizontal: "center"},
+ })
+
+ f.SetColWidth(sheet, "A", "A", 25)
+ f.SetColWidth(sheet, "B", "B", 40)
+ f.SetColWidth(sheet, "C", "C", 15)
+
+ headers := []string{"Sektion", "Titel", "Status"}
+ cols := []string{"A", "B", "C"}
+
+ for i, h := range headers {
+ f.SetCellValue(sheet, cellRef(cols[i], 1), h)
+ }
+ f.SetCellStyle(sheet, "A1", cellRef(cols[len(cols)-1], 1), headerStyle)
+
+ for i, s := range sections {
+ row := i + 2
+ f.SetCellValue(sheet, cellRef("A", row), s.SectionType)
+ f.SetCellValue(sheet, cellRef("B", row), s.Title)
+ f.SetCellValue(sheet, cellRef("C", row), string(s.Status))
+ }
+}
diff --git a/ai-compliance-sdk/internal/iace/document_export_helpers.go b/ai-compliance-sdk/internal/iace/document_export_helpers.go
new file mode 100644
index 0000000..1893317
--- /dev/null
+++ b/ai-compliance-sdk/internal/iace/document_export_helpers.go
@@ -0,0 +1,198 @@
+package iace
+
+import (
+ "archive/zip"
+ "bytes"
+ "encoding/xml"
+ "fmt"
+)
+
+// buildAssessmentMap builds a map from hazardID string to the latest RiskAssessment.
+func buildAssessmentMap(assessments []RiskAssessment) map[string]*RiskAssessment {
+ m := make(map[string]*RiskAssessment)
+ for i := range assessments {
+ a := &assessments[i]
+ key := a.HazardID.String()
+ if existing, ok := m[key]; !ok || a.Version > existing.Version {
+ m[key] = a
+ }
+ }
+ return m
+}
+
+// riskLevelColor returns RGB values for PDF fill color based on risk level.
+func riskLevelColor(level RiskLevel) (r, g, b int) {
+ switch level {
+ case RiskLevelNotAcceptable:
+ return 180, 0, 0
+ case RiskLevelVeryHigh:
+ return 220, 40, 40
+ case RiskLevelCritical:
+ return 255, 80, 80
+ case RiskLevelHigh:
+ return 255, 165, 80
+ case RiskLevelMedium:
+ return 255, 230, 100
+ case RiskLevelLow:
+ return 180, 230, 140
+ case RiskLevelNegligible:
+ return 140, 210, 140
+ default:
+ return 240, 240, 240
+ }
+}
+
+// riskLevelLabel returns a German display label for a risk level.
+func riskLevelLabel(level RiskLevel) string {
+ switch level {
+ case RiskLevelNotAcceptable:
+ return "Nicht akzeptabel"
+ case RiskLevelVeryHigh:
+ return "Sehr hoch"
+ case RiskLevelCritical:
+ return "Kritisch"
+ case RiskLevelHigh:
+ return "Hoch"
+ case RiskLevelMedium:
+ return "Mittel"
+ case RiskLevelLow:
+ return "Niedrig"
+ case RiskLevelNegligible:
+ return "Vernachlaessigbar"
+ default:
+ return string(level)
+ }
+}
+
+// reductionTypeLabel returns a German label for a reduction type.
+func reductionTypeLabel(rt ReductionType) string {
+ switch rt {
+ case ReductionTypeDesign:
+ return "Konstruktiv"
+ case ReductionTypeProtective:
+ return "Schutzmassnahme"
+ case ReductionTypeInformation:
+ return "Information"
+ default:
+ return string(rt)
+ }
+}
+
+// mitigationStatusLabel returns a German label for a mitigation status.
+func mitigationStatusLabel(status MitigationStatus) string {
+ switch status {
+ case MitigationStatusPlanned:
+ return "Geplant"
+ case MitigationStatusImplemented:
+ return "Umgesetzt"
+ case MitigationStatusVerified:
+ return "Verifiziert"
+ case MitigationStatusRejected:
+ return "Abgelehnt"
+ default:
+ return string(status)
+ }
+}
+
+// regulationLabel returns a German label for a regulation type.
+func regulationLabel(reg RegulationType) string {
+ switch reg {
+ case RegulationNIS2:
+ return "NIS-2 Richtlinie"
+ case RegulationAIAct:
+ return "EU AI Act"
+ case RegulationCRA:
+ return "Cyber Resilience Act"
+ case RegulationMachineryRegulation:
+ return "EU Maschinenverordnung 2023/1230"
+ default:
+ return string(reg)
+ }
+}
+
+// escapeXML escapes special XML characters in text content.
+func escapeXML(s string) string {
+ var buf bytes.Buffer
+ if err := xml.EscapeText(&buf, []byte(s)); err != nil {
+ return s
+ }
+ return buf.String()
+}
+
+// countByRiskLevel counts assessments per risk level.
+func countByRiskLevel(assessments []RiskAssessment) map[RiskLevel]int {
+ counts := make(map[RiskLevel]int)
+ for _, a := range assessments {
+ counts[a.RiskLevel]++
+ }
+ return counts
+}
+
+// pdfTruncate truncates a string for PDF cell display.
+func pdfTruncate(s string, maxLen int) string {
+ runes := []rune(s)
+ if len(runes) <= maxLen {
+ return s
+ }
+ if maxLen <= 3 {
+ return string(runes[:maxLen])
+ }
+ return string(runes[:maxLen-3]) + "..."
+}
+
+// cellRef builds an Excel cell reference like "A1", "B12".
+func cellRef(col string, row int) string {
+ return fmt.Sprintf("%s%d", col, row)
+}
+
+// rgbHex converts RGB values to a hex color string (without #).
+func rgbHex(r, g, b int) string {
+ return fmt.Sprintf("%02X%02X%02X", r, g, b)
+}
+
+// addZipEntry writes a text file into a zip archive.
+func addZipEntry(zw *zip.Writer, name, content string) error {
+ w, err := zw.Create(name)
+ if err != nil {
+ return err
+ }
+ _, err = w.Write([]byte(content))
+ return err
+}
+
+// docxHeading builds a DOCX paragraph with a heading style.
+func docxHeading(text string, level int) string {
+ sizes := map[int]int{1: 64, 2: 52, 3: 44}
+ sz, ok := sizes[level]
+ if !ok {
+ sz = 44
+ }
+ escaped := escapeXML(text)
+ return fmt.Sprintf(`
+
+
+
+
+
+
+ %s
+
+
+`, level, sz, sz, escaped)
+}
+
+// docxParagraph builds a DOCX paragraph, optionally italic.
+func docxParagraph(text string, italic bool) string {
+ escaped := escapeXML(text)
+ rpr := ""
+ if italic {
+ rpr = ""
+ }
+ return fmt.Sprintf(`
+
+ %s
+ %s
+
+
+`, rpr, escaped)
+}
diff --git a/ai-compliance-sdk/internal/iace/document_export_pdf.go b/ai-compliance-sdk/internal/iace/document_export_pdf.go
new file mode 100644
index 0000000..b074ad1
--- /dev/null
+++ b/ai-compliance-sdk/internal/iace/document_export_pdf.go
@@ -0,0 +1,313 @@
+package iace
+
+import (
+ "fmt"
+ "time"
+
+ "github.com/jung-kurt/gofpdf"
+)
+
+func (e *DocumentExporter) pdfCoverPage(pdf *gofpdf.Fpdf, project *Project) {
+ pdf.Ln(60)
+
+ pdf.SetFont("Helvetica", "B", 28)
+ pdf.SetTextColor(0, 0, 0)
+ pdf.CellFormat(0, 15, "CE-Technische Akte", "", 1, "C", false, 0, "")
+ pdf.Ln(5)
+
+ pdf.SetFont("Helvetica", "B", 22)
+ pdf.CellFormat(0, 12, project.MachineName, "", 1, "C", false, 0, "")
+ pdf.Ln(15)
+
+ pdf.SetFont("Helvetica", "", 12)
+ coverItems := []struct {
+ label string
+ value string
+ }{
+ {"Hersteller", project.Manufacturer},
+ {"Maschinentyp", project.MachineType},
+ {"CE-Kennzeichnungsziel", project.CEMarkingTarget},
+ {"Projektstatus", string(project.Status)},
+ {"Datum", time.Now().Format("02.01.2006")},
+ }
+
+ for _, item := range coverItems {
+ if item.value == "" {
+ continue
+ }
+ pdf.SetFont("Helvetica", "B", 12)
+ pdf.CellFormat(60, 8, item.label+":", "", 0, "R", false, 0, "")
+ pdf.SetFont("Helvetica", "", 12)
+ pdf.CellFormat(5, 8, "", "", 0, "", false, 0, "")
+ pdf.CellFormat(0, 8, item.value, "", 1, "L", false, 0, "")
+ }
+
+ if project.Description != "" {
+ pdf.Ln(15)
+ pdf.SetFont("Helvetica", "I", 10)
+ pdf.MultiCell(0, 5, project.Description, "", "C", false)
+ }
+}
+
+func (e *DocumentExporter) pdfTableOfContents(pdf *gofpdf.Fpdf, sections []TechFileSection) {
+ pdf.SetFont("Helvetica", "B", 16)
+ pdf.SetTextColor(50, 50, 50)
+ pdf.CellFormat(0, 10, "Inhaltsverzeichnis", "", 1, "L", false, 0, "")
+ pdf.SetTextColor(0, 0, 0)
+ pdf.SetDrawColor(200, 200, 200)
+ pdf.Line(10, pdf.GetY(), 200, pdf.GetY())
+ pdf.Ln(8)
+
+ pdf.SetFont("Helvetica", "", 11)
+
+ fixedEntries := []string{
+ "Gefaehrdungsprotokoll",
+ "Risikomatrix-Zusammenfassung",
+ "Massnahmen-Uebersicht",
+ }
+
+ pageEstimate := 3
+ for i, section := range sections {
+ pdf.CellFormat(10, 7, fmt.Sprintf("%d.", i+1), "", 0, "R", false, 0, "")
+ pdf.CellFormat(5, 7, "", "", 0, "", false, 0, "")
+ pdf.CellFormat(130, 7, section.Title, "", 0, "L", false, 0, "")
+ pdf.CellFormat(0, 7, fmt.Sprintf("~%d", pageEstimate+i), "", 1, "R", false, 0, "")
+ }
+
+ startPage := pageEstimate + len(sections)
+ for i, entry := range fixedEntries {
+ idx := len(sections) + i + 1
+ pdf.CellFormat(10, 7, fmt.Sprintf("%d.", idx), "", 0, "R", false, 0, "")
+ pdf.CellFormat(5, 7, "", "", 0, "", false, 0, "")
+ pdf.CellFormat(130, 7, entry, "", 0, "L", false, 0, "")
+ pdf.CellFormat(0, 7, fmt.Sprintf("~%d", startPage+i), "", 1, "R", false, 0, "")
+ }
+}
+
+func (e *DocumentExporter) pdfSection(pdf *gofpdf.Fpdf, section TechFileSection) {
+ pdf.SetFont("Helvetica", "B", 14)
+ pdf.SetTextColor(50, 50, 50)
+ pdf.CellFormat(0, 10, section.Title, "", 1, "L", false, 0, "")
+ pdf.SetTextColor(0, 0, 0)
+
+ pdf.SetFont("Helvetica", "I", 9)
+ pdf.SetTextColor(100, 100, 100)
+ pdf.CellFormat(0, 5,
+ fmt.Sprintf("Typ: %s | Status: %s | Version: %d",
+ section.SectionType, string(section.Status), section.Version),
+ "", 1, "L", false, 0, "")
+ pdf.SetTextColor(0, 0, 0)
+
+ pdf.SetDrawColor(200, 200, 200)
+ pdf.Line(10, pdf.GetY(), 200, pdf.GetY())
+ pdf.Ln(5)
+
+ pdf.SetFont("Helvetica", "", 10)
+ if section.Content != "" {
+ pdf.MultiCell(0, 5, section.Content, "", "L", false)
+ } else {
+ pdf.SetFont("Helvetica", "I", 10)
+ pdf.SetTextColor(150, 150, 150)
+ pdf.CellFormat(0, 7, "(Kein Inhalt vorhanden)", "", 1, "L", false, 0, "")
+ pdf.SetTextColor(0, 0, 0)
+ }
+}
+
+func (e *DocumentExporter) pdfHazardLog(pdf *gofpdf.Fpdf, hazards []Hazard, assessments []RiskAssessment) {
+ pdf.SetFont("Helvetica", "B", 14)
+ pdf.SetTextColor(50, 50, 50)
+ pdf.CellFormat(0, 10, "Gefaehrdungsprotokoll", "", 1, "L", false, 0, "")
+ pdf.SetTextColor(0, 0, 0)
+ pdf.SetDrawColor(200, 200, 200)
+ pdf.Line(10, pdf.GetY(), 200, pdf.GetY())
+ pdf.Ln(5)
+
+ if len(hazards) == 0 {
+ pdf.SetFont("Helvetica", "I", 10)
+ pdf.CellFormat(0, 7, "(Keine Gefaehrdungen erfasst)", "", 1, "L", false, 0, "")
+ return
+ }
+
+ assessMap := buildAssessmentMap(assessments)
+
+ colWidths := []float64{10, 40, 30, 12, 12, 12, 30, 20}
+ headers := []string{"Nr", "Name", "Kategorie", "S", "E", "P", "Risiko", "OK"}
+
+ pdf.SetFont("Helvetica", "B", 9)
+ pdf.SetFillColor(240, 240, 240)
+ for i, h := range headers {
+ pdf.CellFormat(colWidths[i], 7, h, "1", 0, "C", true, 0, "")
+ }
+ pdf.Ln(-1)
+
+ pdf.SetFont("Helvetica", "", 8)
+ for i, hazard := range hazards {
+ if pdf.GetY() > 265 {
+ pdf.AddPage()
+ pdf.SetFont("Helvetica", "B", 9)
+ pdf.SetFillColor(240, 240, 240)
+ for j, h := range headers {
+ pdf.CellFormat(colWidths[j], 7, h, "1", 0, "C", true, 0, "")
+ }
+ pdf.Ln(-1)
+ pdf.SetFont("Helvetica", "", 8)
+ }
+
+ a := assessMap[hazard.ID.String()]
+
+ sev, exp, prob := "", "", ""
+ riskLabel := "-"
+ acceptable := "-"
+ var rl RiskLevel
+
+ if a != nil {
+ sev = fmt.Sprintf("%d", a.Severity)
+ exp = fmt.Sprintf("%d", a.Exposure)
+ prob = fmt.Sprintf("%d", a.Probability)
+ rl = a.RiskLevel
+ riskLabel = riskLevelLabel(rl)
+ if a.IsAcceptable {
+ acceptable = "Ja"
+ } else {
+ acceptable = "Nein"
+ }
+ }
+
+ r, g, b := riskLevelColor(rl)
+ pdf.SetFillColor(r, g, b)
+ fill := rl != ""
+
+ pdf.CellFormat(colWidths[0], 6, fmt.Sprintf("%d", i+1), "1", 0, "C", fill, 0, "")
+ pdf.CellFormat(colWidths[1], 6, pdfTruncate(hazard.Name, 22), "1", 0, "L", fill, 0, "")
+ pdf.CellFormat(colWidths[2], 6, pdfTruncate(hazard.Category, 16), "1", 0, "L", fill, 0, "")
+ pdf.CellFormat(colWidths[3], 6, sev, "1", 0, "C", fill, 0, "")
+ pdf.CellFormat(colWidths[4], 6, exp, "1", 0, "C", fill, 0, "")
+ pdf.CellFormat(colWidths[5], 6, prob, "1", 0, "C", fill, 0, "")
+ pdf.CellFormat(colWidths[6], 6, riskLabel, "1", 0, "C", fill, 0, "")
+ pdf.CellFormat(colWidths[7], 6, acceptable, "1", 0, "C", fill, 0, "")
+ pdf.Ln(-1)
+ }
+}
+
+func (e *DocumentExporter) pdfRiskMatrixSummary(pdf *gofpdf.Fpdf, assessments []RiskAssessment) {
+ pdf.Ln(10)
+ pdf.SetFont("Helvetica", "B", 14)
+ pdf.SetTextColor(50, 50, 50)
+ pdf.CellFormat(0, 10, "Risikomatrix-Zusammenfassung", "", 1, "L", false, 0, "")
+ pdf.SetTextColor(0, 0, 0)
+ pdf.SetDrawColor(200, 200, 200)
+ pdf.Line(10, pdf.GetY(), 200, pdf.GetY())
+ pdf.Ln(5)
+
+ counts := countByRiskLevel(assessments)
+
+ levels := []RiskLevel{
+ RiskLevelNotAcceptable,
+ RiskLevelVeryHigh,
+ RiskLevelCritical,
+ RiskLevelHigh,
+ RiskLevelMedium,
+ RiskLevelLow,
+ RiskLevelNegligible,
+ }
+
+ pdf.SetFont("Helvetica", "B", 9)
+ pdf.SetFillColor(240, 240, 240)
+ pdf.CellFormat(60, 7, "Risikostufe", "1", 0, "L", true, 0, "")
+ pdf.CellFormat(30, 7, "Anzahl", "1", 0, "C", true, 0, "")
+ pdf.Ln(-1)
+
+ pdf.SetFont("Helvetica", "", 9)
+ for _, level := range levels {
+ count := counts[level]
+ if count == 0 {
+ continue
+ }
+ r, g, b := riskLevelColor(level)
+ pdf.SetFillColor(r, g, b)
+ pdf.CellFormat(60, 6, riskLevelLabel(level), "1", 0, "L", true, 0, "")
+ pdf.CellFormat(30, 6, fmt.Sprintf("%d", count), "1", 0, "C", true, 0, "")
+ pdf.Ln(-1)
+ }
+
+ pdf.SetFont("Helvetica", "B", 9)
+ pdf.SetFillColor(240, 240, 240)
+ pdf.CellFormat(60, 7, "Gesamt", "1", 0, "L", true, 0, "")
+ pdf.CellFormat(30, 7, fmt.Sprintf("%d", len(assessments)), "1", 0, "C", true, 0, "")
+ pdf.Ln(-1)
+}
+
+func (e *DocumentExporter) pdfMitigationsTable(pdf *gofpdf.Fpdf, mitigations []Mitigation) {
+ pdf.SetFont("Helvetica", "B", 14)
+ pdf.SetTextColor(50, 50, 50)
+ pdf.CellFormat(0, 10, "Massnahmen-Uebersicht", "", 1, "L", false, 0, "")
+ pdf.SetTextColor(0, 0, 0)
+ pdf.SetDrawColor(200, 200, 200)
+ pdf.Line(10, pdf.GetY(), 200, pdf.GetY())
+ pdf.Ln(5)
+
+ if len(mitigations) == 0 {
+ pdf.SetFont("Helvetica", "I", 10)
+ pdf.CellFormat(0, 7, "(Keine Massnahmen erfasst)", "", 1, "L", false, 0, "")
+ return
+ }
+
+ colWidths := []float64{10, 45, 30, 30, 40}
+ headers := []string{"Nr", "Name", "Typ", "Status", "Verifikation"}
+
+ pdf.SetFont("Helvetica", "B", 9)
+ pdf.SetFillColor(240, 240, 240)
+ for i, h := range headers {
+ pdf.CellFormat(colWidths[i], 7, h, "1", 0, "C", true, 0, "")
+ }
+ pdf.Ln(-1)
+
+ pdf.SetFont("Helvetica", "", 8)
+ for i, m := range mitigations {
+ if pdf.GetY() > 265 {
+ pdf.AddPage()
+ pdf.SetFont("Helvetica", "B", 9)
+ pdf.SetFillColor(240, 240, 240)
+ for j, h := range headers {
+ pdf.CellFormat(colWidths[j], 7, h, "1", 0, "C", true, 0, "")
+ }
+ pdf.Ln(-1)
+ pdf.SetFont("Helvetica", "", 8)
+ }
+
+ pdf.CellFormat(colWidths[0], 6, fmt.Sprintf("%d", i+1), "1", 0, "C", false, 0, "")
+ pdf.CellFormat(colWidths[1], 6, pdfTruncate(m.Name, 25), "1", 0, "L", false, 0, "")
+ pdf.CellFormat(colWidths[2], 6, reductionTypeLabel(m.ReductionType), "1", 0, "C", false, 0, "")
+ pdf.CellFormat(colWidths[3], 6, mitigationStatusLabel(m.Status), "1", 0, "C", false, 0, "")
+ pdf.CellFormat(colWidths[4], 6, pdfTruncate(string(m.VerificationMethod), 22), "1", 0, "L", false, 0, "")
+ pdf.Ln(-1)
+ }
+}
+
+func (e *DocumentExporter) pdfClassifications(pdf *gofpdf.Fpdf, classifications []RegulatoryClassification) {
+ pdf.SetFont("Helvetica", "B", 14)
+ pdf.SetTextColor(50, 50, 50)
+ pdf.CellFormat(0, 10, "Regulatorische Klassifizierungen", "", 1, "L", false, 0, "")
+ pdf.SetTextColor(0, 0, 0)
+ pdf.SetDrawColor(200, 200, 200)
+ pdf.Line(10, pdf.GetY(), 200, pdf.GetY())
+ pdf.Ln(5)
+
+ for _, c := range classifications {
+ pdf.SetFont("Helvetica", "B", 11)
+ pdf.CellFormat(0, 7, regulationLabel(c.Regulation), "", 1, "L", false, 0, "")
+
+ pdf.SetFont("Helvetica", "", 10)
+ pdf.CellFormat(50, 6, "Klassifizierung:", "", 0, "L", false, 0, "")
+ pdf.CellFormat(0, 6, c.ClassificationResult, "", 1, "L", false, 0, "")
+
+ pdf.CellFormat(50, 6, "Risikostufe:", "", 0, "L", false, 0, "")
+ pdf.CellFormat(0, 6, riskLevelLabel(c.RiskLevel), "", 1, "L", false, 0, "")
+
+ if c.Reasoning != "" {
+ pdf.CellFormat(50, 6, "Begruendung:", "", 0, "L", false, 0, "")
+ pdf.MultiCell(0, 5, c.Reasoning, "", "L", false)
+ }
+ pdf.Ln(5)
+ }
+}
diff --git a/ai-compliance-sdk/internal/iace/hazard_library.go b/ai-compliance-sdk/internal/iace/hazard_library.go
index b480c2d..65b3ef9 100644
--- a/ai-compliance-sdk/internal/iace/hazard_library.go
+++ b/ai-compliance-sdk/internal/iace/hazard_library.go
@@ -3,7 +3,6 @@ package iace
import (
"encoding/json"
"fmt"
- "time"
"github.com/google/uuid"
)
@@ -32,3117 +31,13 @@ func mustMarshalJSON(v interface{}) json.RawMessage {
// All entries have IsBuiltin=true and TenantID=nil (system-level templates).
// UUIDs are deterministic, generated via uuid.NewSHA1 based on category and index.
func GetBuiltinHazardLibrary() []HazardLibraryEntry {
- now := time.Now()
-
- entries := []HazardLibraryEntry{
- // ====================================================================
- // Category: false_classification (4 entries)
- // ====================================================================
- {
- ID: hazardUUID("false_classification", 1),
- Category: "false_classification",
- Name: "Falsche Bauteil-Klassifikation durch KI",
- Description: "Das KI-Modell klassifiziert ein Bauteil fehlerhaft, was zu falscher Weiterverarbeitung oder Montage fuehren kann.",
- DefaultSeverity: 4,
- DefaultProbability: 3,
- ApplicableComponentTypes: []string{"ai_model", "sensor"},
- RegulationReferences: []string{"EU AI Act Art. 9", "Maschinenverordnung 2023/1230"},
- SuggestedMitigations: mustMarshalJSON([]string{"Redundante Pruefung", "Konfidenz-Schwellwert"}),
- IsBuiltin: true,
- TenantID: nil,
- CreatedAt: now,
- },
- {
- ID: hazardUUID("false_classification", 2),
- Category: "false_classification",
- Name: "Falsche Qualitaetsentscheidung (IO/NIO)",
- Description: "Fehlerhafte IO/NIO-Entscheidung durch das KI-System fuehrt dazu, dass defekte Teile als gut bewertet oder gute Teile verworfen werden.",
- DefaultSeverity: 4,
- DefaultProbability: 3,
- ApplicableComponentTypes: []string{"ai_model", "software"},
- RegulationReferences: []string{"EU AI Act Art. 9", "Maschinenverordnung 2023/1230"},
- SuggestedMitigations: mustMarshalJSON([]string{"Human-in-the-Loop", "Stichproben-Gegenpruefung"}),
- IsBuiltin: true,
- TenantID: nil,
- CreatedAt: now,
- },
- {
- ID: hazardUUID("false_classification", 3),
- Category: "false_classification",
- Name: "Fehlklassifikation bei Grenzwertfaellen",
- Description: "Bauteile nahe an Toleranzgrenzen werden systematisch falsch klassifiziert, da das Modell in Grenzwertbereichen unsicher agiert.",
- DefaultSeverity: 3,
- DefaultProbability: 4,
- ApplicableComponentTypes: []string{"ai_model"},
- RegulationReferences: []string{"EU AI Act Art. 9", "ISO 13849"},
- SuggestedMitigations: mustMarshalJSON([]string{"Erweitertes Training", "Grauzone-Eskalation"}),
- IsBuiltin: true,
- TenantID: nil,
- CreatedAt: now,
- },
- {
- ID: hazardUUID("false_classification", 4),
- Category: "false_classification",
- Name: "Verwechslung von Bauteiltypen",
- Description: "Unterschiedliche Bauteiltypen werden vom KI-Modell verwechselt, was zu falscher Montage oder Verarbeitung fuehrt.",
- DefaultSeverity: 4,
- DefaultProbability: 2,
- ApplicableComponentTypes: []string{"ai_model", "sensor"},
- RegulationReferences: []string{"EU AI Act Art. 9", "Maschinenverordnung 2023/1230"},
- SuggestedMitigations: mustMarshalJSON([]string{"Barcode-Gegenpruefung", "Doppelte Sensorik"}),
- IsBuiltin: true,
- TenantID: nil,
- CreatedAt: now,
- },
-
- // ====================================================================
- // Category: timing_error (3 entries)
- // ====================================================================
- {
- ID: hazardUUID("timing_error", 1),
- Category: "timing_error",
- Name: "Verzoegerte KI-Reaktion in Echtzeitsystem",
- Description: "Die KI-Inferenz dauert laenger als die zulaessige Echtzeitfrist, was zu verspaeteten Sicherheitsreaktionen fuehrt.",
- DefaultSeverity: 5,
- DefaultProbability: 2,
- ApplicableComponentTypes: []string{"software", "ai_model"},
- RegulationReferences: []string{"Maschinenverordnung 2023/1230", "ISO 13849", "IEC 62443"},
- SuggestedMitigations: mustMarshalJSON([]string{"Watchdog-Timer", "Fallback-Steuerung"}),
- IsBuiltin: true,
- TenantID: nil,
- CreatedAt: now,
- },
- {
- ID: hazardUUID("timing_error", 2),
- Category: "timing_error",
- Name: "Echtzeit-Verletzung Safety-Loop",
- Description: "Der sicherheitsgerichtete Regelkreis kann die geforderten Zykluszeiten nicht einhalten, wodurch Sicherheitsfunktionen versagen koennen.",
- DefaultSeverity: 5,
- DefaultProbability: 2,
- ApplicableComponentTypes: []string{"software", "firmware"},
- RegulationReferences: []string{"ISO 13849", "IEC 61508", "Maschinenverordnung 2023/1230"},
- SuggestedMitigations: mustMarshalJSON([]string{"Deterministische Ausfuehrung", "WCET-Analyse"}),
- IsBuiltin: true,
- TenantID: nil,
- CreatedAt: now,
- },
- {
- ID: hazardUUID("timing_error", 3),
- Category: "timing_error",
- Name: "Timing-Jitter bei Netzwerkkommunikation",
- Description: "Schwankende Netzwerklatenzen fuehren zu unvorhersehbaren Verzoegerungen in der Datenuebertragung sicherheitsrelevanter Signale.",
- DefaultSeverity: 3,
- DefaultProbability: 3,
- ApplicableComponentTypes: []string{"network", "software"},
- RegulationReferences: []string{"IEC 62443", "Maschinenverordnung 2023/1230"},
- SuggestedMitigations: mustMarshalJSON([]string{"TSN-Netzwerk", "Pufferung"}),
- IsBuiltin: true,
- TenantID: nil,
- CreatedAt: now,
- },
-
- // ====================================================================
- // Category: data_poisoning (2 entries)
- // ====================================================================
- {
- ID: hazardUUID("data_poisoning", 1),
- Category: "data_poisoning",
- Name: "Manipulierte Trainingsdaten",
- Description: "Trainingsdaten werden absichtlich oder unbeabsichtigt manipuliert, wodurch das Modell systematisch fehlerhafte Entscheidungen trifft.",
- DefaultSeverity: 4,
- DefaultProbability: 2,
- ApplicableComponentTypes: []string{"ai_model"},
- RegulationReferences: []string{"EU AI Act Art. 10", "CRA"},
- SuggestedMitigations: mustMarshalJSON([]string{"Daten-Validierung", "Anomalie-Erkennung"}),
- IsBuiltin: true,
- TenantID: nil,
- CreatedAt: now,
- },
- {
- ID: hazardUUID("data_poisoning", 2),
- Category: "data_poisoning",
- Name: "Adversarial Input Angriff",
- Description: "Gezielte Manipulation von Eingabedaten (z.B. Bilder, Sensorsignale), um das KI-Modell zu taeuschen und Fehlentscheidungen auszuloesen.",
- DefaultSeverity: 4,
- DefaultProbability: 2,
- ApplicableComponentTypes: []string{"ai_model", "sensor"},
- RegulationReferences: []string{"EU AI Act Art. 15", "CRA", "IEC 62443"},
- SuggestedMitigations: mustMarshalJSON([]string{"Input-Validation", "Adversarial Training"}),
- IsBuiltin: true,
- TenantID: nil,
- CreatedAt: now,
- },
-
- // ====================================================================
- // Category: model_drift (3 entries)
- // ====================================================================
- {
- ID: hazardUUID("model_drift", 1),
- Category: "model_drift",
- Name: "Performance-Degradation durch Concept Drift",
- Description: "Die statistische Verteilung der Eingabedaten aendert sich ueber die Zeit, wodurch die Modellgenauigkeit schleichend abnimmt.",
- DefaultSeverity: 3,
- DefaultProbability: 4,
- ApplicableComponentTypes: []string{"ai_model"},
- RegulationReferences: []string{"EU AI Act Art. 9", "EU AI Act Art. 72"},
- SuggestedMitigations: mustMarshalJSON([]string{"Monitoring-Dashboard", "Automatisches Retraining"}),
- IsBuiltin: true,
- TenantID: nil,
- CreatedAt: now,
- },
- {
- ID: hazardUUID("model_drift", 2),
- Category: "model_drift",
- Name: "Data Drift durch veraenderte Umgebung",
- Description: "Aenderungen in der physischen Umgebung (Beleuchtung, Temperatur, Material) fuehren zu veraenderten Sensordaten und Modellfehlern.",
- DefaultSeverity: 3,
- DefaultProbability: 4,
- ApplicableComponentTypes: []string{"ai_model", "sensor"},
- RegulationReferences: []string{"EU AI Act Art. 9", "Maschinenverordnung 2023/1230"},
- SuggestedMitigations: mustMarshalJSON([]string{"Statistische Ueberwachung", "Sensor-Kalibrierung"}),
- IsBuiltin: true,
- TenantID: nil,
- CreatedAt: now,
- },
- {
- ID: hazardUUID("model_drift", 3),
- Category: "model_drift",
- Name: "Schleichende Modell-Verschlechterung",
- Description: "Ohne aktives Monitoring verschlechtert sich die Modellqualitaet ueber Wochen oder Monate unbemerkt.",
- DefaultSeverity: 3,
- DefaultProbability: 3,
- ApplicableComponentTypes: []string{"ai_model"},
- RegulationReferences: []string{"EU AI Act Art. 9", "EU AI Act Art. 72"},
- SuggestedMitigations: mustMarshalJSON([]string{"Regelmaessige Evaluierung", "A/B-Testing"}),
- IsBuiltin: true,
- TenantID: nil,
- CreatedAt: now,
- },
-
- // ====================================================================
- // Category: sensor_spoofing (3 entries)
- // ====================================================================
- {
- ID: hazardUUID("sensor_spoofing", 1),
- Category: "sensor_spoofing",
- Name: "Kamera-Manipulation / Abdeckung",
- Description: "Kamerasensoren werden absichtlich oder unbeabsichtigt abgedeckt oder manipuliert, sodass das System auf Basis falscher Bilddaten agiert.",
- DefaultSeverity: 4,
- DefaultProbability: 2,
- ApplicableComponentTypes: []string{"sensor"},
- RegulationReferences: []string{"IEC 62443", "Maschinenverordnung 2023/1230"},
- SuggestedMitigations: mustMarshalJSON([]string{"Plausibilitaetspruefung", "Mehrfach-Sensorik"}),
- IsBuiltin: true,
- TenantID: nil,
- CreatedAt: now,
- },
- {
- ID: hazardUUID("sensor_spoofing", 2),
- Category: "sensor_spoofing",
- Name: "Sensor-Signal-Injection",
- Description: "Einspeisung gefaelschter Signale in die Sensorleitungen oder Schnittstellen, um das System gezielt zu manipulieren.",
- DefaultSeverity: 5,
- DefaultProbability: 1,
- ApplicableComponentTypes: []string{"sensor", "network"},
- RegulationReferences: []string{"IEC 62443", "CRA"},
- SuggestedMitigations: mustMarshalJSON([]string{"Signalverschluesselung", "Anomalie-Erkennung"}),
- IsBuiltin: true,
- TenantID: nil,
- CreatedAt: now,
- },
- {
- ID: hazardUUID("sensor_spoofing", 3),
- Category: "sensor_spoofing",
- Name: "Umgebungsbasierte Sensor-Taeuschung",
- Description: "Natuerliche oder kuenstliche Umgebungsveraenderungen (Licht, Staub, Vibration) fuehren zu fehlerhaften Sensorwerten.",
- DefaultSeverity: 3,
- DefaultProbability: 3,
- ApplicableComponentTypes: []string{"sensor"},
- RegulationReferences: []string{"Maschinenverordnung 2023/1230", "ISO 13849"},
- SuggestedMitigations: mustMarshalJSON([]string{"Sensor-Fusion", "Redundanz"}),
- IsBuiltin: true,
- TenantID: nil,
- CreatedAt: now,
- },
-
- // ====================================================================
- // Category: communication_failure (3 entries)
- // ====================================================================
- {
- ID: hazardUUID("communication_failure", 1),
- Category: "communication_failure",
- Name: "Feldbus-Ausfall",
- Description: "Ausfall des industriellen Feldbusses (z.B. PROFINET, EtherCAT) fuehrt zum Verlust der Kommunikation zwischen Steuerung und Aktorik.",
- DefaultSeverity: 4,
- DefaultProbability: 3,
- ApplicableComponentTypes: []string{"network", "controller"},
- RegulationReferences: []string{"Maschinenverordnung 2023/1230", "ISO 13849", "IEC 62443"},
- SuggestedMitigations: mustMarshalJSON([]string{"Redundanter Bus", "Safe-State-Transition"}),
- IsBuiltin: true,
- TenantID: nil,
- CreatedAt: now,
- },
- {
- ID: hazardUUID("communication_failure", 2),
- Category: "communication_failure",
- Name: "Cloud-Verbindungsverlust",
- Description: "Die Verbindung zur Cloud-Infrastruktur bricht ab, wodurch cloud-abhaengige Funktionen (z.B. Modell-Updates, Monitoring) nicht verfuegbar sind.",
- DefaultSeverity: 3,
- DefaultProbability: 4,
- ApplicableComponentTypes: []string{"network", "software"},
- RegulationReferences: []string{"CRA", "EU AI Act Art. 15"},
- SuggestedMitigations: mustMarshalJSON([]string{"Offline-Faehigkeit", "Edge-Computing"}),
- IsBuiltin: true,
- TenantID: nil,
- CreatedAt: now,
- },
- {
- ID: hazardUUID("communication_failure", 3),
- Category: "communication_failure",
- Name: "Netzwerk-Latenz-Spitzen",
- Description: "Unkontrollierte Latenzspitzen im Netzwerk fuehren zu Timeouts und verspaeteter Datenlieferung an sicherheitsrelevante Systeme.",
- DefaultSeverity: 3,
- DefaultProbability: 3,
- ApplicableComponentTypes: []string{"network"},
- RegulationReferences: []string{"IEC 62443", "Maschinenverordnung 2023/1230"},
- SuggestedMitigations: mustMarshalJSON([]string{"QoS-Konfiguration", "Timeout-Handling"}),
- IsBuiltin: true,
- TenantID: nil,
- CreatedAt: now,
- },
-
- // ====================================================================
- // Category: unauthorized_access (4 entries)
- // ====================================================================
- {
- ID: hazardUUID("unauthorized_access", 1),
- Category: "unauthorized_access",
- Name: "Unautorisierter Remote-Zugriff",
- Description: "Ein Angreifer erlangt ueber das Netzwerk Zugriff auf die Maschinensteuerung und kann sicherheitsrelevante Parameter aendern.",
- DefaultSeverity: 5,
- DefaultProbability: 2,
- ApplicableComponentTypes: []string{"network", "software"},
- RegulationReferences: []string{"IEC 62443", "CRA", "EU AI Act Art. 15"},
- SuggestedMitigations: mustMarshalJSON([]string{"VPN", "MFA", "Netzwerksegmentierung"}),
- IsBuiltin: true,
- TenantID: nil,
- CreatedAt: now,
- },
- {
- ID: hazardUUID("unauthorized_access", 2),
- Category: "unauthorized_access",
- Name: "Konfigurations-Manipulation",
- Description: "Sicherheitsrelevante Konfigurationsparameter werden unautorisiert geaendert, z.B. Grenzwerte, Schwellwerte oder Betriebsmodi.",
- DefaultSeverity: 5,
- DefaultProbability: 2,
- ApplicableComponentTypes: []string{"software", "firmware"},
- RegulationReferences: []string{"IEC 62443", "CRA", "Maschinenverordnung 2023/1230"},
- SuggestedMitigations: mustMarshalJSON([]string{"Zugriffskontrolle", "Audit-Log"}),
- IsBuiltin: true,
- TenantID: nil,
- CreatedAt: now,
- },
- {
- ID: hazardUUID("unauthorized_access", 3),
- Category: "unauthorized_access",
- Name: "Privilege Escalation",
- Description: "Ein Benutzer oder Prozess erlangt hoehere Berechtigungen als vorgesehen und kann sicherheitskritische Aktionen ausfuehren.",
- DefaultSeverity: 5,
- DefaultProbability: 1,
- ApplicableComponentTypes: []string{"software"},
- RegulationReferences: []string{"IEC 62443", "CRA"},
- SuggestedMitigations: mustMarshalJSON([]string{"RBAC", "Least Privilege"}),
- IsBuiltin: true,
- TenantID: nil,
- CreatedAt: now,
- },
- {
- ID: hazardUUID("unauthorized_access", 4),
- Category: "unauthorized_access",
- Name: "Supply-Chain-Angriff auf Komponente",
- Description: "Eine kompromittierte Softwarekomponente oder Firmware wird ueber die Lieferkette eingeschleust und enthaelt Schadcode oder Backdoors.",
- DefaultSeverity: 5,
- DefaultProbability: 1,
- ApplicableComponentTypes: []string{"software", "firmware"},
- RegulationReferences: []string{"CRA", "IEC 62443", "EU AI Act Art. 15"},
- SuggestedMitigations: mustMarshalJSON([]string{"SBOM", "Signaturpruefung"}),
- IsBuiltin: true,
- TenantID: nil,
- CreatedAt: now,
- },
-
- // ====================================================================
- // Category: firmware_corruption (3 entries)
- // ====================================================================
- {
- ID: hazardUUID("firmware_corruption", 1),
- Category: "firmware_corruption",
- Name: "Update-Abbruch mit inkonsistentem Zustand",
- Description: "Ein Firmware-Update wird unterbrochen (z.B. Stromausfall), wodurch das System in einem inkonsistenten und potenziell unsicheren Zustand verbleibt.",
- DefaultSeverity: 5,
- DefaultProbability: 2,
- ApplicableComponentTypes: []string{"firmware"},
- RegulationReferences: []string{"CRA", "Maschinenverordnung 2023/1230"},
- SuggestedMitigations: mustMarshalJSON([]string{"A/B-Partitioning", "Rollback"}),
- IsBuiltin: true,
- TenantID: nil,
- CreatedAt: now,
- },
- {
- ID: hazardUUID("firmware_corruption", 2),
- Category: "firmware_corruption",
- Name: "Rollback-Fehler auf alte Version",
- Description: "Ein Rollback auf eine aeltere Firmware-Version schlaegt fehl oder fuehrt zu Inkompatibilitaeten mit der aktuellen Hardware-/Softwarekonfiguration.",
- DefaultSeverity: 4,
- DefaultProbability: 2,
- ApplicableComponentTypes: []string{"firmware"},
- RegulationReferences: []string{"CRA", "Maschinenverordnung 2023/1230"},
- SuggestedMitigations: mustMarshalJSON([]string{"Versionsmanagement", "Kompatibilitaetspruefung"}),
- IsBuiltin: true,
- TenantID: nil,
- CreatedAt: now,
- },
- {
- ID: hazardUUID("firmware_corruption", 3),
- Category: "firmware_corruption",
- Name: "Boot-Chain-Angriff",
- Description: "Die Bootsequenz wird manipuliert, um unsignierte oder kompromittierte Firmware auszufuehren, was die gesamte Sicherheitsarchitektur untergaebt.",
- DefaultSeverity: 5,
- DefaultProbability: 1,
- ApplicableComponentTypes: []string{"firmware"},
- RegulationReferences: []string{"CRA", "IEC 62443"},
- SuggestedMitigations: mustMarshalJSON([]string{"Secure Boot", "TPM"}),
- IsBuiltin: true,
- TenantID: nil,
- CreatedAt: now,
- },
-
- // ====================================================================
- // Category: safety_boundary_violation (4 entries)
- // ====================================================================
- {
- ID: hazardUUID("safety_boundary_violation", 1),
- Category: "safety_boundary_violation",
- Name: "Kraft-/Drehmoment-Ueberschreitung",
- Description: "Aktorische Systeme ueberschreiten die zulaessigen Kraft- oder Drehmomentwerte, was zu Verletzungen oder Maschinenschaeden fuehren kann.",
- DefaultSeverity: 5,
- DefaultProbability: 2,
- ApplicableComponentTypes: []string{"controller", "actuator"},
- RegulationReferences: []string{"Maschinenverordnung 2023/1230", "ISO 13849", "IEC 62061"},
- SuggestedMitigations: mustMarshalJSON([]string{"Hardware-Limiter", "SIL-Ueberwachung"}),
- IsBuiltin: true,
- TenantID: nil,
- CreatedAt: now,
- },
- {
- ID: hazardUUID("safety_boundary_violation", 2),
- Category: "safety_boundary_violation",
- Name: "Geschwindigkeitsueberschreitung Roboter",
- Description: "Ein Industrieroboter ueberschreitet die zulaessige Geschwindigkeit, insbesondere bei Mensch-Roboter-Kollaboration.",
- DefaultSeverity: 5,
- DefaultProbability: 2,
- ApplicableComponentTypes: []string{"controller", "software"},
- RegulationReferences: []string{"Maschinenverordnung 2023/1230", "ISO 13849", "ISO 10218"},
- SuggestedMitigations: mustMarshalJSON([]string{"Safe Speed Monitoring", "Lichtgitter"}),
- IsBuiltin: true,
- TenantID: nil,
- CreatedAt: now,
- },
- {
- ID: hazardUUID("safety_boundary_violation", 3),
- Category: "safety_boundary_violation",
- Name: "Versagen des Safe-State",
- Description: "Das System kann im Fehlerfall keinen sicheren Zustand einnehmen, da die Sicherheitssteuerung selbst versagt.",
- DefaultSeverity: 5,
- DefaultProbability: 1,
- ApplicableComponentTypes: []string{"controller", "software", "firmware"},
- RegulationReferences: []string{"Maschinenverordnung 2023/1230", "ISO 13849", "IEC 62061"},
- SuggestedMitigations: mustMarshalJSON([]string{"Redundante Sicherheitssteuerung", "Diverse Programmierung"}),
- IsBuiltin: true,
- TenantID: nil,
- CreatedAt: now,
- },
- {
- ID: hazardUUID("safety_boundary_violation", 4),
- Category: "safety_boundary_violation",
- Name: "Arbeitsraum-Verletzung",
- Description: "Ein Roboter oder Aktor verlaesst seinen definierten Arbeitsraum und dringt in den Schutzbereich von Personen ein.",
- DefaultSeverity: 5,
- DefaultProbability: 2,
- ApplicableComponentTypes: []string{"controller", "sensor"},
- RegulationReferences: []string{"Maschinenverordnung 2023/1230", "ISO 13849", "ISO 10218"},
- SuggestedMitigations: mustMarshalJSON([]string{"Sichere Achsueberwachung", "Schutzzaun-Sensorik"}),
- IsBuiltin: true,
- TenantID: nil,
- CreatedAt: now,
- },
-
- // ====================================================================
- // Category: mode_confusion (3 entries)
- // ====================================================================
- {
- ID: hazardUUID("mode_confusion", 1),
- Category: "mode_confusion",
- Name: "Falsche Betriebsart aktiv",
- Description: "Das System befindet sich in einer unbeabsichtigten Betriebsart (z.B. Automatik statt Einrichtbetrieb), was zu unerwarteten Maschinenbewegungen fuehrt.",
- DefaultSeverity: 4,
- DefaultProbability: 3,
- ApplicableComponentTypes: []string{"hmi", "software"},
- RegulationReferences: []string{"Maschinenverordnung 2023/1230", "ISO 13849"},
- SuggestedMitigations: mustMarshalJSON([]string{"Betriebsart-Anzeige", "Schluesselschalter"}),
- IsBuiltin: true,
- TenantID: nil,
- CreatedAt: now,
- },
- {
- ID: hazardUUID("mode_confusion", 2),
- Category: "mode_confusion",
- Name: "Wartung/Normal-Verwechslung",
- Description: "Das System wird im Normalbetrieb gewartet oder der Wartungsmodus wird nicht korrekt verlassen, was zu gefaehrlichen Situationen fuehrt.",
- DefaultSeverity: 5,
- DefaultProbability: 2,
- ApplicableComponentTypes: []string{"hmi", "software"},
- RegulationReferences: []string{"Maschinenverordnung 2023/1230", "ISO 13849"},
- SuggestedMitigations: mustMarshalJSON([]string{"Zugangskontrolle", "Sicherheitsverriegelung"}),
- IsBuiltin: true,
- TenantID: nil,
- CreatedAt: now,
- },
- {
- ID: hazardUUID("mode_confusion", 3),
- Category: "mode_confusion",
- Name: "Automatik-Eingriff waehrend Handbetrieb",
- Description: "Das System wechselt waehrend des Handbetriebs unerwartet in den Automatikbetrieb, wodurch eine Person im Gefahrenbereich verletzt werden kann.",
- DefaultSeverity: 5,
- DefaultProbability: 2,
- ApplicableComponentTypes: []string{"software", "controller"},
- RegulationReferences: []string{"Maschinenverordnung 2023/1230", "ISO 13849"},
- SuggestedMitigations: mustMarshalJSON([]string{"Exklusive Betriebsarten", "Zustimmtaster"}),
- IsBuiltin: true,
- TenantID: nil,
- CreatedAt: now,
- },
-
- // ====================================================================
- // Category: unintended_bias (2 entries)
- // ====================================================================
- {
- ID: hazardUUID("unintended_bias", 1),
- Category: "unintended_bias",
- Name: "Diskriminierende KI-Entscheidung",
- Description: "Das KI-Modell trifft systematisch diskriminierende Entscheidungen, z.B. bei der Qualitaetsbewertung bestimmter Produktchargen oder Lieferanten.",
- DefaultSeverity: 3,
- DefaultProbability: 2,
- ApplicableComponentTypes: []string{"ai_model"},
- RegulationReferences: []string{"EU AI Act Art. 10", "EU AI Act Art. 71"},
- SuggestedMitigations: mustMarshalJSON([]string{"Bias-Testing", "Fairness-Metriken"}),
- IsBuiltin: true,
- TenantID: nil,
- CreatedAt: now,
- },
- {
- ID: hazardUUID("unintended_bias", 2),
- Category: "unintended_bias",
- Name: "Verzerrte Trainingsdaten",
- Description: "Die Trainingsdaten sind nicht repraesentativ und enthalten systematische Verzerrungen, die zu unfairen oder fehlerhaften Modellergebnissen fuehren.",
- DefaultSeverity: 3,
- DefaultProbability: 3,
- ApplicableComponentTypes: []string{"ai_model"},
- RegulationReferences: []string{"EU AI Act Art. 10", "EU AI Act Art. 71"},
- SuggestedMitigations: mustMarshalJSON([]string{"Datensatz-Audit", "Ausgewogenes Sampling"}),
- IsBuiltin: true,
- TenantID: nil,
- CreatedAt: now,
- },
-
- // ====================================================================
- // Category: update_failure (3 entries)
- // ====================================================================
- {
- ID: hazardUUID("update_failure", 1),
- Category: "update_failure",
- Name: "Unvollstaendiges OTA-Update",
- Description: "Ein Over-the-Air-Update wird nur teilweise uebertragen oder angewendet, wodurch das System in einem inkonsistenten Zustand verbleibt.",
- DefaultSeverity: 4,
- DefaultProbability: 3,
- ApplicableComponentTypes: []string{"firmware", "software"},
- RegulationReferences: []string{"CRA", "Maschinenverordnung 2023/1230"},
- SuggestedMitigations: mustMarshalJSON([]string{"Atomare Updates", "Integritaetspruefung"}),
- IsBuiltin: true,
- TenantID: nil,
- CreatedAt: now,
- },
- {
- ID: hazardUUID("update_failure", 2),
- Category: "update_failure",
- Name: "Versionskonflikt nach Update",
- Description: "Nach einem Update sind Software- und Firmware-Versionen inkompatibel, was zu Fehlfunktionen oder Ausfaellen fuehrt.",
- DefaultSeverity: 3,
- DefaultProbability: 3,
- ApplicableComponentTypes: []string{"software", "firmware"},
- RegulationReferences: []string{"CRA", "Maschinenverordnung 2023/1230"},
- SuggestedMitigations: mustMarshalJSON([]string{"Kompatibilitaetsmatrix", "Staging-Tests"}),
- IsBuiltin: true,
- TenantID: nil,
- CreatedAt: now,
- },
- {
- ID: hazardUUID("update_failure", 3),
- Category: "update_failure",
- Name: "Unkontrollierter Auto-Update",
- Description: "Ein automatisches Update wird ohne Genehmigung oder ausserhalb eines Wartungsfensters eingespielt und stoert den laufenden Betrieb.",
- DefaultSeverity: 4,
- DefaultProbability: 2,
- ApplicableComponentTypes: []string{"software"},
- RegulationReferences: []string{"CRA", "Maschinenverordnung 2023/1230", "IEC 62443"},
- SuggestedMitigations: mustMarshalJSON([]string{"Update-Genehmigung", "Wartungsfenster"}),
- IsBuiltin: true,
- TenantID: nil,
- CreatedAt: now,
- },
- }
-
- // ====================================================================
- // Neue Kategorien (extended library ~100 neue Eintraege)
- // ====================================================================
-
- extended := []HazardLibraryEntry{
- // ====================================================================
- // Category: software_fault (10 entries)
- // ====================================================================
- {
- ID: hazardUUID("software_fault", 1),
- Category: "software_fault",
- Name: "Race Condition in Sicherheitsfunktion",
- Description: "Zwei Tasks greifen ohne Synchronisation auf gemeinsame Ressourcen zu, was zu unvorhersehbarem Verhalten in sicherheitsrelevanten Funktionen fuehren kann.",
- DefaultSeverity: 5,
- DefaultProbability: 3,
- ApplicableComponentTypes: []string{"software", "firmware"},
- RegulationReferences: []string{"EU 2023/1230 Anhang I §1.2", "IEC 62304", "IEC 61508"},
- SuggestedMitigations: mustMarshalJSON([]string{"Mutex/Semaphor", "RTOS-Task-Prioritaeten", "WCET-Analyse"}),
- IsBuiltin: true,
- TenantID: nil,
- CreatedAt: now,
- },
- {
- ID: hazardUUID("software_fault", 2),
- Category: "software_fault",
- Name: "Stack Overflow in Echtzeit-Task",
- Description: "Ein rekursiver Aufruf oder grosse lokale Variablen fuehren zum Stack-Ueberlauf, was Safety-Tasks zum Absturz bringt.",
- DefaultSeverity: 4,
- DefaultProbability: 3,
- ApplicableComponentTypes: []string{"software", "firmware"},
- RegulationReferences: []string{"IEC 62304", "IEC 61508"},
- SuggestedMitigations: mustMarshalJSON([]string{"Stack-Groessen-Analyse", "Stack-Guard", "Statische Code-Analyse"}),
- IsBuiltin: true,
- TenantID: nil,
- CreatedAt: now,
- },
- {
- ID: hazardUUID("software_fault", 3),
- Category: "software_fault",
- Name: "Integer Overflow in Sicherheitsberechnung",
- Description: "Arithmetischer Ueberlauf bei der Berechnung sicherheitskritischer Grenzwerte fuehrt zu falschen Ergebnissen und unkontrolliertem Verhalten.",
- DefaultSeverity: 5,
- DefaultProbability: 2,
- ApplicableComponentTypes: []string{"software", "firmware"},
- RegulationReferences: []string{"IEC 62304", "MISRA-C", "IEC 61508"},
- SuggestedMitigations: mustMarshalJSON([]string{"Datentyp-Pruefung", "Overflow-Detection", "MISRA-C-Analyse"}),
- IsBuiltin: true,
- TenantID: nil,
- CreatedAt: now,
- },
- {
- ID: hazardUUID("software_fault", 4),
- Category: "software_fault",
- Name: "Deadlock zwischen Safety-Tasks",
- Description: "Gegenseitige Sperrung von Tasks durch zyklische Ressourcenabhaengigkeiten verhindert die Ausfuehrung sicherheitsrelevanter Funktionen.",
- DefaultSeverity: 4,
- DefaultProbability: 3,
- ApplicableComponentTypes: []string{"software", "firmware"},
- RegulationReferences: []string{"IEC 62304", "IEC 61508"},
- SuggestedMitigations: mustMarshalJSON([]string{"Ressourcen-Hierarchie", "Watchdog", "Deadlock-Analyse"}),
- IsBuiltin: true,
- TenantID: nil,
- CreatedAt: now,
- },
- {
- ID: hazardUUID("software_fault", 5),
- Category: "software_fault",
- Name: "Memory Leak im Langzeitbetrieb",
- Description: "Nicht freigegebener Heap-Speicher akkumuliert sich ueber Zeit, bis das System abstuerzt und Sicherheitsfunktionen nicht mehr verfuegbar sind.",
- DefaultSeverity: 3,
- DefaultProbability: 4,
- ApplicableComponentTypes: []string{"software"},
- RegulationReferences: []string{"IEC 62304", "IEC 61508"},
- SuggestedMitigations: mustMarshalJSON([]string{"Memory-Profiling", "Valgrind", "Statisches Speichermanagement"}),
- IsBuiltin: true,
- TenantID: nil,
- CreatedAt: now,
- },
- {
- ID: hazardUUID("software_fault", 6),
- Category: "software_fault",
- Name: "Null-Pointer-Dereferenz in Safety-Code",
- Description: "Zugriff auf einen Null-Zeiger fuehrt zu einem undefinierten Systemzustand oder Absturz des sicherheitsrelevanten Software-Moduls.",
- DefaultSeverity: 4,
- DefaultProbability: 3,
- ApplicableComponentTypes: []string{"software", "firmware"},
- RegulationReferences: []string{"IEC 62304", "MISRA-C"},
- SuggestedMitigations: mustMarshalJSON([]string{"Null-Check vor Zugriff", "Statische Analyse", "Defensiv-Programmierung"}),
- IsBuiltin: true,
- TenantID: nil,
- CreatedAt: now,
- },
- {
- ID: hazardUUID("software_fault", 7),
- Category: "software_fault",
- Name: "Unbehandelte Ausnahme in Safety-Code",
- Description: "Eine nicht abgefangene Ausnahme bricht die Ausfuehrung des sicherheitsrelevanten Codes ab und hinterlaesst das System in einem undefinierten Zustand.",
- DefaultSeverity: 5,
- DefaultProbability: 2,
- ApplicableComponentTypes: []string{"software"},
- RegulationReferences: []string{"IEC 62304", "IEC 61508"},
- SuggestedMitigations: mustMarshalJSON([]string{"Globaler Exception-Handler", "Exception-Safety-Analyse", "Fail-Safe-Rueckfall"}),
- IsBuiltin: true,
- TenantID: nil,
- CreatedAt: now,
- },
- {
- ID: hazardUUID("software_fault", 8),
- Category: "software_fault",
- Name: "Korrupte Konfigurationsdaten",
- Description: "Beschaedigte oder unvollstaendige Konfigurationsdaten werden ohne Validierung geladen und fuehren zu falschem Systemverhalten.",
- DefaultSeverity: 4,
- DefaultProbability: 3,
- ApplicableComponentTypes: []string{"software", "firmware"},
- RegulationReferences: []string{"IEC 62304", "Maschinenverordnung 2023/1230"},
- SuggestedMitigations: mustMarshalJSON([]string{"Konfig-Validierung", "CRC-Pruefung", "Fallback-Konfiguration"}),
- IsBuiltin: true,
- TenantID: nil,
- CreatedAt: now,
- },
- {
- ID: hazardUUID("software_fault", 9),
- Category: "software_fault",
- Name: "Division durch Null in Regelkreis",
- Description: "Ein Divisor im sicherheitsrelevanten Regelkreis erreicht den Wert Null, was zu einem Laufzeitfehler oder undefiniertem Ergebnis fuehrt.",
- DefaultSeverity: 5,
- DefaultProbability: 2,
- ApplicableComponentTypes: []string{"software", "firmware"},
- RegulationReferences: []string{"IEC 62304", "IEC 61508", "MISRA-C"},
- SuggestedMitigations: mustMarshalJSON([]string{"Vorbedingungspruefung", "Statische Analyse", "Defensiv-Programmierung"}),
- IsBuiltin: true,
- TenantID: nil,
- CreatedAt: now,
- },
- {
- ID: hazardUUID("software_fault", 10),
- Category: "software_fault",
- Name: "Falscher Safety-Parameter durch Software-Bug",
- Description: "Ein Software-Fehler setzt einen sicherheitsrelevanten Parameter auf einen falschen Wert, ohne dass eine Plausibilitaetspruefung dies erkennt.",
- DefaultSeverity: 5,
- DefaultProbability: 2,
- ApplicableComponentTypes: []string{"software", "firmware"},
- RegulationReferences: []string{"IEC 62304", "IEC 61508", "Maschinenverordnung 2023/1230"},
- SuggestedMitigations: mustMarshalJSON([]string{"Parametervalidierung", "Redundante Speicherung", "Diversitaere Pruefung"}),
- IsBuiltin: true,
- TenantID: nil,
- CreatedAt: now,
- },
-
- // ====================================================================
- // Category: hmi_error (8 entries)
- // ====================================================================
- {
- ID: hazardUUID("hmi_error", 1),
- Category: "hmi_error",
- Name: "Falsche Einheitendarstellung",
- Description: "Das HMI zeigt Werte in einer falschen Masseinheit an (z.B. mm statt inch), was zu Fehlbedienung und Maschinenfehlern fuehren kann.",
- DefaultSeverity: 4,
- DefaultProbability: 3,
- ApplicableComponentTypes: []string{"hmi", "software"},
- RegulationReferences: []string{"Maschinenverordnung 2023/1230 Anhang III", "EN ISO 9241"},
- SuggestedMitigations: mustMarshalJSON([]string{"Einheiten-Label im UI", "Lokalisierungstests", "Einheiten-Konvertierungspruefung"}),
- IsBuiltin: true,
- TenantID: nil,
- CreatedAt: now,
- },
- {
- ID: hazardUUID("hmi_error", 2),
- Category: "hmi_error",
- Name: "Fehlender oder stummer Sicherheitsalarm",
- Description: "Ein kritisches Sicherheitsereignis wird dem Bediener nicht oder nicht rechtzeitig angezeigt, weil die Alarmfunktion deaktiviert oder fehlerhaft ist.",
- DefaultSeverity: 5,
- DefaultProbability: 2,
- ApplicableComponentTypes: []string{"hmi", "software"},
- RegulationReferences: []string{"Maschinenverordnung 2023/1230", "ISO 13849", "EN ISO 9241"},
- SuggestedMitigations: mustMarshalJSON([]string{"Alarmtest im Rahmen der Inbetriebnahme", "Akustischer Backup-Alarm", "Alarmverwaltungssystem"}),
- IsBuiltin: true,
- TenantID: nil,
- CreatedAt: now,
- },
- {
- ID: hazardUUID("hmi_error", 3),
- Category: "hmi_error",
- Name: "Sprachfehler in Bedienoberflaeche",
- Description: "Fehlerhafte oder mehrdeutige Bezeichnungen in der Benutzersprache fuehren zu Fehlbedienung sicherheitsrelevanter Funktionen.",
- DefaultSeverity: 3,
- DefaultProbability: 3,
- ApplicableComponentTypes: []string{"hmi"},
- RegulationReferences: []string{"Maschinenverordnung 2023/1230 Anhang III"},
- SuggestedMitigations: mustMarshalJSON([]string{"Usability-Test", "Lokalisierungs-Review", "Mehrsprachige Dokumentation"}),
- IsBuiltin: true,
- TenantID: nil,
- CreatedAt: now,
- },
- {
- ID: hazardUUID("hmi_error", 4),
- Category: "hmi_error",
- Name: "Fehlende Eingabevalidierung im HMI",
- Description: "Das HMI akzeptiert ausserhalb des gueltigen Bereichs liegende Eingaben ohne Warnung und leitet sie an die Steuerung weiter.",
- DefaultSeverity: 4,
- DefaultProbability: 3,
- ApplicableComponentTypes: []string{"hmi", "software"},
- RegulationReferences: []string{"Maschinenverordnung 2023/1230", "IEC 62304"},
- SuggestedMitigations: mustMarshalJSON([]string{"Grenzwertpruefung", "Eingabemaske mit Bereichen", "Warnung bei Grenzwertnaehe"}),
- IsBuiltin: true,
- TenantID: nil,
- CreatedAt: now,
- },
- {
- ID: hazardUUID("hmi_error", 5),
- Category: "hmi_error",
- Name: "Defekter Statusindikator",
- Description: "Ein LED, Anzeigeelement oder Softwaresymbol zeigt einen falschen Systemstatus an und verleitet den Bediener zu falschen Annahmen.",
- DefaultSeverity: 4,
- DefaultProbability: 3,
- ApplicableComponentTypes: []string{"hmi"},
- RegulationReferences: []string{"Maschinenverordnung 2023/1230 Anhang III"},
- SuggestedMitigations: mustMarshalJSON([]string{"Regelmaessige HMI-Tests", "Selbsttest beim Einschalten", "Redundante Statusanzeige"}),
- IsBuiltin: true,
- TenantID: nil,
- CreatedAt: now,
- },
- {
- ID: hazardUUID("hmi_error", 6),
- Category: "hmi_error",
- Name: "Quittierung ohne Ursachenbehebung",
- Description: "Der Bediener kann einen Sicherheitsalarm quittieren, ohne die zugrundeliegende Ursache behoben zu haben, was das Risiko wiederkehrender Ereignisse erhoet.",
- DefaultSeverity: 4,
- DefaultProbability: 3,
- ApplicableComponentTypes: []string{"hmi", "software"},
- RegulationReferences: []string{"Maschinenverordnung 2023/1230", "ISO 13849"},
- SuggestedMitigations: mustMarshalJSON([]string{"Ursachen-Checkliste vor Quittierung", "Pflicht-Ursachen-Eingabe", "Audit-Log der Quittierungen"}),
- IsBuiltin: true,
- TenantID: nil,
- CreatedAt: now,
- },
- {
- ID: hazardUUID("hmi_error", 7),
- Category: "hmi_error",
- Name: "Veraltete Anzeige durch Caching-Fehler",
- Description: "Die HMI-Anzeige wird nicht aktualisiert und zeigt veraltete Sensorwerte oder Zustaende an, was zu Fehlentscheidungen fuehrt.",
- DefaultSeverity: 4,
- DefaultProbability: 3,
- ApplicableComponentTypes: []string{"hmi", "software"},
- RegulationReferences: []string{"Maschinenverordnung 2023/1230 Anhang III"},
- SuggestedMitigations: mustMarshalJSON([]string{"Timestamp-Anzeige", "Refresh-Watchdog", "Verbindungsstatus-Indikator"}),
- IsBuiltin: true,
- TenantID: nil,
- CreatedAt: now,
- },
- {
- ID: hazardUUID("hmi_error", 8),
- Category: "hmi_error",
- Name: "Fehlende Betriebsart-Kennzeichnung",
- Description: "Die aktive Betriebsart (Automatik, Einrichten, Wartung) ist im HMI nicht eindeutig sichtbar, was zu unerwarteten Maschinenbewegungen fuehren kann.",
- DefaultSeverity: 4,
- DefaultProbability: 3,
- ApplicableComponentTypes: []string{"hmi", "software"},
- RegulationReferences: []string{"Maschinenverordnung 2023/1230", "ISO 13849"},
- SuggestedMitigations: mustMarshalJSON([]string{"Dauerhafte Betriebsart-Anzeige", "Farbliche Kennzeichnung", "Bestaetigung bei Modewechsel"}),
- IsBuiltin: true,
- TenantID: nil,
- CreatedAt: now,
- },
-
- // ====================================================================
- // Category: mechanical_hazard (6 entries)
- // ====================================================================
- {
- ID: hazardUUID("mechanical_hazard", 1),
- Category: "mechanical_hazard",
- Name: "Unerwarteter Anlauf nach Spannungsausfall",
- Description: "Nach Wiederkehr der Versorgungsspannung laeuft die Maschine unerwartet an, ohne dass eine Startfreigabe durch den Bediener erfolgt ist.",
- DefaultSeverity: 5,
- DefaultProbability: 3,
- ApplicableComponentTypes: []string{"controller", "firmware"},
- RegulationReferences: []string{"Maschinenverordnung 2023/1230 Anhang I §1.2.6", "ISO 13849"},
- SuggestedMitigations: mustMarshalJSON([]string{"Anlaufschutz", "Anti-Restart-Funktion", "Sicherheitsrelais"}),
- IsBuiltin: true,
- TenantID: nil,
- CreatedAt: now,
- },
- {
- ID: hazardUUID("mechanical_hazard", 2),
- Category: "mechanical_hazard",
- Name: "Restenergie nach Abschalten",
- Description: "Gespeicherte kinetische oder potentielle Energie (z.B. Schwungmasse, abgesenktes Werkzeug) wird nach dem Abschalten nicht sicher abgebaut.",
- DefaultSeverity: 5,
- DefaultProbability: 2,
- ApplicableComponentTypes: []string{"actuator", "controller"},
- RegulationReferences: []string{"Maschinenverordnung 2023/1230 Anhang I §1.6", "ISO 13849"},
- SuggestedMitigations: mustMarshalJSON([]string{"Energieabbau-Prozedur", "Mechanische Haltevorrichtung", "LOTO-Freischaltung"}),
- IsBuiltin: true,
- TenantID: nil,
- CreatedAt: now,
- },
- {
- ID: hazardUUID("mechanical_hazard", 3),
- Category: "mechanical_hazard",
- Name: "Unerwartete Maschinenbewegung",
- Description: "Die Maschine fuehrt eine unkontrollierte Bewegung durch (z.B. Antrieb faehrt ohne Kommando los), was Personen im Gefahrenbereich verletzt.",
- DefaultSeverity: 5,
- DefaultProbability: 2,
- ApplicableComponentTypes: []string{"actuator", "controller", "software"},
- RegulationReferences: []string{"Maschinenverordnung 2023/1230", "ISO 13849", "IEC 62061"},
- SuggestedMitigations: mustMarshalJSON([]string{"Safe Torque Off (STO)", "Geschwindigkeitsueberwachung", "Schutzzaun-Sensorik"}),
- IsBuiltin: true,
- TenantID: nil,
- CreatedAt: now,
- },
- {
- ID: hazardUUID("mechanical_hazard", 4),
- Category: "mechanical_hazard",
- Name: "Teileauswurf durch Fehlfunktion",
- Description: "Werkzeuge, Werkstuecke oder Maschinenteile werden bei einer Fehlfunktion unkontrolliert aus der Maschine geschleudert.",
- DefaultSeverity: 5,
- DefaultProbability: 2,
- ApplicableComponentTypes: []string{"actuator", "controller"},
- RegulationReferences: []string{"Maschinenverordnung 2023/1230 Anhang I §1.3.2"},
- SuggestedMitigations: mustMarshalJSON([]string{"Trennende Schutzeinrichtung", "Sicherheitsglas", "Spannkraft-Ueberwachung"}),
- IsBuiltin: true,
- TenantID: nil,
- CreatedAt: now,
- },
- {
- ID: hazardUUID("mechanical_hazard", 5),
- Category: "mechanical_hazard",
- Name: "Quetschstelle durch fehlende Schutzeinrichtung",
- Description: "Zwischen beweglichen und festen Maschinenteilen entstehen Quetschstellen, die bei fehlendem Schutz zu schweren Verletzungen fuehren koennen.",
- DefaultSeverity: 5,
- DefaultProbability: 2,
- ApplicableComponentTypes: []string{"actuator"},
- RegulationReferences: []string{"Maschinenverordnung 2023/1230 Anhang I §1.3.7", "ISO 13857"},
- SuggestedMitigations: mustMarshalJSON([]string{"Schutzverkleidung", "Sicherheitsabstaende nach ISO 13857", "Lichtvorhang"}),
- IsBuiltin: true,
- TenantID: nil,
- CreatedAt: now,
- },
- {
- ID: hazardUUID("mechanical_hazard", 6),
- Category: "mechanical_hazard",
- Name: "Instabile Struktur unter Last",
- Description: "Die Maschinenstruktur oder ein Anbauteil versagt unter statischer oder dynamischer Belastung und gefaehrdet Personen in der Naehe.",
- DefaultSeverity: 5,
- DefaultProbability: 1,
- ApplicableComponentTypes: []string{"other"},
- RegulationReferences: []string{"Maschinenverordnung 2023/1230 Anhang I §1.3.1"},
- SuggestedMitigations: mustMarshalJSON([]string{"Festigkeitsberechnung", "Ueberlastsicherung", "Regelmaessige Inspektion"}),
- IsBuiltin: true,
- TenantID: nil,
- CreatedAt: now,
- },
-
- // ====================================================================
- // Category: electrical_hazard (6 entries)
- // ====================================================================
- {
- ID: hazardUUID("electrical_hazard", 1),
- Category: "electrical_hazard",
- Name: "Elektrischer Schlag an Bedienpanel",
- Description: "Bediener kommen mit spannungsfuehrenden Teilen in Beruehrung, z.B. durch defekte Gehaeuseerdung oder fehlerhafte Isolierung.",
- DefaultSeverity: 5,
- DefaultProbability: 2,
- ApplicableComponentTypes: []string{"hmi", "controller"},
- RegulationReferences: []string{"Niederspannungsrichtlinie 2014/35/EU", "IEC 60204-1"},
- SuggestedMitigations: mustMarshalJSON([]string{"Schutzkleinspannung (SELV)", "Schutzerdung", "Isolationsmonitoring"}),
- IsBuiltin: true,
- TenantID: nil,
- CreatedAt: now,
- },
- {
- ID: hazardUUID("electrical_hazard", 2),
- Category: "electrical_hazard",
- Name: "Lichtbogen durch Schaltfehler",
- Description: "Ein Schaltfehler erzeugt einen Lichtbogen, der Bediener verletzen, Geraete beschaedigen oder einen Brand ausloesen kann.",
- DefaultSeverity: 5,
- DefaultProbability: 1,
- ApplicableComponentTypes: []string{"controller"},
- RegulationReferences: []string{"Niederspannungsrichtlinie 2014/35/EU", "IEC 60204-1"},
- SuggestedMitigations: mustMarshalJSON([]string{"Lichtbogenschutz-Schalter", "Kurzschlussschutz", "Geeignete Schaltgeraete"}),
- IsBuiltin: true,
- TenantID: nil,
- CreatedAt: now,
- },
- {
- ID: hazardUUID("electrical_hazard", 3),
- Category: "electrical_hazard",
- Name: "Kurzschluss durch Isolationsfehler",
- Description: "Beschaedigte Kabelisolierungen fuehren zu einem Kurzschluss, der Feuer ausloesen oder Sicherheitsfunktionen ausser Betrieb setzen kann.",
- DefaultSeverity: 4,
- DefaultProbability: 2,
- ApplicableComponentTypes: []string{"network", "controller"},
- RegulationReferences: []string{"Niederspannungsrichtlinie 2014/35/EU", "IEC 60204-1"},
- SuggestedMitigations: mustMarshalJSON([]string{"Isolationsueberwachung", "Kabelschutz", "Regelmaessige Sichtpruefung"}),
- IsBuiltin: true,
- TenantID: nil,
- CreatedAt: now,
- },
- {
- ID: hazardUUID("electrical_hazard", 4),
- Category: "electrical_hazard",
- Name: "Erdschluss in Steuerkreis",
- Description: "Ein Erdschluss im Steuerkreis kann unbeabsichtigte Schaltzustaende ausloesen und Sicherheitsfunktionen beeinflussen.",
- DefaultSeverity: 4,
- DefaultProbability: 2,
- ApplicableComponentTypes: []string{"controller", "network"},
- RegulationReferences: []string{"IEC 60204-1", "ISO 13849"},
- SuggestedMitigations: mustMarshalJSON([]string{"Erdschluss-Monitoring", "IT-Netz fuer Steuerkreise", "Regelmaessige Pruefung"}),
- IsBuiltin: true,
- TenantID: nil,
- CreatedAt: now,
- },
- {
- ID: hazardUUID("electrical_hazard", 5),
- Category: "electrical_hazard",
- Name: "Gespeicherte Energie in Kondensatoren",
- Description: "Nach dem Abschalten verbleiben hohe Spannungen in Kondensatoren (z.B. Frequenzumrichter, USV), was bei Wartungsarbeiten gefaehrlich ist.",
- DefaultSeverity: 5,
- DefaultProbability: 2,
- ApplicableComponentTypes: []string{"controller"},
- RegulationReferences: []string{"Niederspannungsrichtlinie 2014/35/EU", "IEC 60204-1"},
- SuggestedMitigations: mustMarshalJSON([]string{"Entladewartezeit", "Automatische Entladeschaltung", "Warnhinweise am Geraet"}),
- IsBuiltin: true,
- TenantID: nil,
- CreatedAt: now,
- },
- {
- ID: hazardUUID("electrical_hazard", 6),
- Category: "electrical_hazard",
- Name: "Elektromagnetische Kopplung auf Safety-Leitung",
- Description: "Hochfrequente Stoerfelder koppeln auf ungeschirmte Safety-Leitungen und erzeugen Falschsignale, die Sicherheitsfunktionen fehl ausloesen.",
- DefaultSeverity: 3,
- DefaultProbability: 3,
- ApplicableComponentTypes: []string{"network", "sensor"},
- RegulationReferences: []string{"EMV-Richtlinie 2014/30/EU", "IEC 62061"},
- SuggestedMitigations: mustMarshalJSON([]string{"Geschirmte Kabel", "Raeumliche Trennung", "EMV-Pruefung"}),
- IsBuiltin: true,
- TenantID: nil,
- CreatedAt: now,
- },
-
- // ====================================================================
- // Category: thermal_hazard (4 entries)
- // ====================================================================
- {
- ID: hazardUUID("thermal_hazard", 1),
- Category: "thermal_hazard",
- Name: "Ueberhitzung der Steuereinheit",
- Description: "Die Steuereinheit ueberschreitet die zulaessige Betriebstemperatur durch Lueftungsausfall oder hohe Umgebungstemperatur, was zu Fehlfunktionen fuehrt.",
- DefaultSeverity: 3,
- DefaultProbability: 3,
- ApplicableComponentTypes: []string{"controller", "firmware"},
- RegulationReferences: []string{"Maschinenverordnung 2023/1230", "IEC 60068"},
- SuggestedMitigations: mustMarshalJSON([]string{"Temperaturueberwachung", "Redundante Lueftung", "Thermisches Abschalten"}),
- IsBuiltin: true,
- TenantID: nil,
- CreatedAt: now,
- },
- {
- ID: hazardUUID("thermal_hazard", 2),
- Category: "thermal_hazard",
- Name: "Brandgefahr durch Leistungselektronik",
- Description: "Defekte Leistungshalbleiter oder Kondensatoren in der Leistungselektronik erwaermen sich unkontrolliert und koennen einen Brand ausloesen.",
- DefaultSeverity: 5,
- DefaultProbability: 2,
- ApplicableComponentTypes: []string{"controller"},
- RegulationReferences: []string{"Niederspannungsrichtlinie 2014/35/EU", "IEC 60204-1"},
- SuggestedMitigations: mustMarshalJSON([]string{"Thermosicherungen", "Temperatursensoren", "Brandschutzmassnahmen"}),
- IsBuiltin: true,
- TenantID: nil,
- CreatedAt: now,
- },
- {
- ID: hazardUUID("thermal_hazard", 3),
- Category: "thermal_hazard",
- Name: "Einfrieren bei Tieftemperatur",
- Description: "Sehr tiefe Umgebungstemperaturen fuehren zum Einfrieren von Hydraulikleitungen oder Elektronik und damit zum Ausfall von Sicherheitsfunktionen.",
- DefaultSeverity: 3,
- DefaultProbability: 3,
- ApplicableComponentTypes: []string{"controller", "actuator"},
- RegulationReferences: []string{"Maschinenverordnung 2023/1230", "IEC 60068"},
- SuggestedMitigations: mustMarshalJSON([]string{"Heizung", "Mindestbetriebstemperatur definieren", "Temperatursensor"}),
- IsBuiltin: true,
- TenantID: nil,
- CreatedAt: now,
- },
- {
- ID: hazardUUID("thermal_hazard", 4),
- Category: "thermal_hazard",
- Name: "Waermestress an Kabelisolierung",
- Description: "Langfristige Einwirkung hoher Temperaturen auf Kabelisolierungen fuehrt zu Alterung und Isolationsversagen mit Kurzschlussrisiko.",
- DefaultSeverity: 3,
- DefaultProbability: 3,
- ApplicableComponentTypes: []string{"network", "controller"},
- RegulationReferences: []string{"IEC 60204-1", "Maschinenverordnung 2023/1230"},
- SuggestedMitigations: mustMarshalJSON([]string{"Hitzebestaendige Kabel (z.B. PTFE)", "Kabelverlegung mit Abstand zur Waermequelle", "Regelmaessige Inspektion"}),
- IsBuiltin: true,
- TenantID: nil,
- CreatedAt: now,
- },
-
- // ====================================================================
- // Category: emc_hazard (5 entries)
- // ====================================================================
- {
- ID: hazardUUID("emc_hazard", 1),
- Category: "emc_hazard",
- Name: "EMV-Stoerabstrahlung auf Safety-Bus",
- Description: "Hohe elektromagnetische Stoerabstrahlung aus benachbarten Geraeten stoert den industriellen Safety-Bus (z.B. PROFIsafe) und erzeugt Kommunikationsfehler.",
- DefaultSeverity: 5,
- DefaultProbability: 2,
- ApplicableComponentTypes: []string{"network", "controller"},
- RegulationReferences: []string{"EMV-Richtlinie 2014/30/EU", "IEC 62061", "IEC 61784-3"},
- SuggestedMitigations: mustMarshalJSON([]string{"EMV-gerechte Verkabelung", "Schirmung", "EMC-Pruefung nach EN 55011"}),
- IsBuiltin: true,
- TenantID: nil,
- CreatedAt: now,
- },
- {
- ID: hazardUUID("emc_hazard", 2),
- Category: "emc_hazard",
- Name: "Unbeabsichtigte elektromagnetische Abstrahlung",
- Description: "Die Maschine selbst strahlt starke EM-Felder ab, die andere sicherheitsrelevante Einrichtungen in der Naehe stoeren.",
- DefaultSeverity: 2,
- DefaultProbability: 3,
- ApplicableComponentTypes: []string{"controller", "actuator"},
- RegulationReferences: []string{"EMV-Richtlinie 2014/30/EU"},
- SuggestedMitigations: mustMarshalJSON([]string{"EMV-Filter", "Gehaeuseabschirmung", "CE-Zulassung Frequenzumrichter"}),
- IsBuiltin: true,
- TenantID: nil,
- CreatedAt: now,
- },
- {
- ID: hazardUUID("emc_hazard", 3),
- Category: "emc_hazard",
- Name: "Frequenzumrichter-Stoerung auf Steuerleitung",
- Description: "Der Frequenzumrichter erzeugt hochfrequente Stoerungen, die auf benachbarte Steuerleitungen koppeln und falsche Signale erzeugen.",
- DefaultSeverity: 4,
- DefaultProbability: 3,
- ApplicableComponentTypes: []string{"actuator", "network"},
- RegulationReferences: []string{"EMV-Richtlinie 2014/30/EU", "IEC 60204-1"},
- SuggestedMitigations: mustMarshalJSON([]string{"Motorfilter", "Kabeltrennabstand", "Separate Kabelkanaele"}),
- IsBuiltin: true,
- TenantID: nil,
- CreatedAt: now,
- },
- {
- ID: hazardUUID("emc_hazard", 4),
- Category: "emc_hazard",
- Name: "ESD-Schaden an Elektronik",
- Description: "Elektrostatische Entladung bei Wartung oder Austausch beschaedigt empfindliche Elektronikbauteile, was zu latenten Fehlfunktionen fuehrt.",
- DefaultSeverity: 3,
- DefaultProbability: 3,
- ApplicableComponentTypes: []string{"controller", "firmware"},
- RegulationReferences: []string{"IEC 61000-4-2", "Maschinenverordnung 2023/1230"},
- SuggestedMitigations: mustMarshalJSON([]string{"ESD-Schulung", "ESD-Schutzausruestung", "ESD-gerechte Verpackung"}),
- IsBuiltin: true,
- TenantID: nil,
- CreatedAt: now,
- },
- {
- ID: hazardUUID("emc_hazard", 5),
- Category: "emc_hazard",
- Name: "HF-Stoerung des Sicherheitssensors",
- Description: "Hochfrequenz-Stoerquellen (z.B. Schweissgeraete, Mobiltelefone) beeinflussen die Funktion von Sicherheitssensoren (Lichtvorhang, Scanner).",
- DefaultSeverity: 4,
- DefaultProbability: 3,
- ApplicableComponentTypes: []string{"sensor"},
- RegulationReferences: []string{"EMV-Richtlinie 2014/30/EU", "IEC 61496"},
- SuggestedMitigations: mustMarshalJSON([]string{"EMV-zertifizierte Sicherheitssensoren", "HF-Quellen trennen", "Gegensprechanlagenverbot in Gefahrenzone"}),
- IsBuiltin: true,
- TenantID: nil,
- CreatedAt: now,
- },
-
- // ====================================================================
- // Category: configuration_error (8 entries)
- // ====================================================================
- {
- ID: hazardUUID("configuration_error", 1),
- Category: "configuration_error",
- Name: "Falscher Safety-Parameter bei Inbetriebnahme",
- Description: "Beim Einrichten werden sicherheitsrelevante Parameter (z.B. Maximalgeschwindigkeit, Abschaltgrenzen) falsch konfiguriert und nicht verifiziert.",
- DefaultSeverity: 5,
- DefaultProbability: 3,
- ApplicableComponentTypes: []string{"software", "firmware", "controller"},
- RegulationReferences: []string{"Maschinenverordnung 2023/1230", "ISO 13849", "IEC 62061"},
- SuggestedMitigations: mustMarshalJSON([]string{"Parameterpruefung nach Inbetriebnahme", "4-Augen-Prinzip", "Parameterprotokoll in technischer Akte"}),
- IsBuiltin: true,
- TenantID: nil,
- CreatedAt: now,
- },
- {
- ID: hazardUUID("configuration_error", 2),
- Category: "configuration_error",
- Name: "Factory Reset loescht Sicherheitskonfiguration",
- Description: "Ein Factory Reset setzt alle Parameter auf Werkseinstellungen zurueck, einschliesslich sicherheitsrelevanter Konfigurationen, ohne Warnung.",
- DefaultSeverity: 5,
- DefaultProbability: 2,
- ApplicableComponentTypes: []string{"firmware", "software"},
- RegulationReferences: []string{"IEC 62304", "CRA", "Maschinenverordnung 2023/1230"},
- SuggestedMitigations: mustMarshalJSON([]string{"Separate Safety-Partition", "Bestaetigung vor Reset", "Safety-Config vor Reset sichern"}),
- IsBuiltin: true,
- TenantID: nil,
- CreatedAt: now,
- },
- {
- ID: hazardUUID("configuration_error", 3),
- Category: "configuration_error",
- Name: "Fehlerhafte Parameter-Migration bei Update",
- Description: "Beim Software-Update werden vorhandene Konfigurationsparameter nicht korrekt in das neue Format migriert, was zu falschen Systemeinstellungen fuehrt.",
- DefaultSeverity: 5,
- DefaultProbability: 2,
- ApplicableComponentTypes: []string{"software", "firmware"},
- RegulationReferences: []string{"IEC 62304", "CRA"},
- SuggestedMitigations: mustMarshalJSON([]string{"Migrations-Skript-Tests", "Konfig-Backup vor Update", "Post-Update-Verifikation"}),
- IsBuiltin: true,
- TenantID: nil,
- CreatedAt: now,
- },
- {
- ID: hazardUUID("configuration_error", 4),
- Category: "configuration_error",
- Name: "Konflikthafte redundante Einstellungen",
- Description: "Widersprüchliche Parameter in verschiedenen Konfigurationsdateien oder -ebenen fuehren zu unvorhersehbarem Systemverhalten.",
- DefaultSeverity: 4,
- DefaultProbability: 3,
- ApplicableComponentTypes: []string{"software", "firmware"},
- RegulationReferences: []string{"IEC 62304", "Maschinenverordnung 2023/1230"},
- SuggestedMitigations: mustMarshalJSON([]string{"Konfig-Validierung beim Start", "Einzelne Quelle fuer Safety-Params", "Konsistenzpruefung"}),
- IsBuiltin: true,
- TenantID: nil,
- CreatedAt: now,
- },
- {
- ID: hazardUUID("configuration_error", 5),
- Category: "configuration_error",
- Name: "Hard-coded Credentials in Konfiguration",
- Description: "Passwörter oder Schluessel sind fest im Code oder in Konfigurationsdateien hinterlegt und koennen nicht geaendert werden.",
- DefaultSeverity: 4,
- DefaultProbability: 2,
- ApplicableComponentTypes: []string{"software", "firmware"},
- RegulationReferences: []string{"CRA", "IEC 62443"},
- SuggestedMitigations: mustMarshalJSON([]string{"Secrets-Management", "Kein Hard-Coding", "Credential-Scan im CI"}),
- IsBuiltin: true,
- TenantID: nil,
- CreatedAt: now,
- },
- {
- ID: hazardUUID("configuration_error", 6),
- Category: "configuration_error",
- Name: "Debug-Modus in Produktionsumgebung aktiv",
- Description: "Debug-Schnittstellen oder erhoehte Logging-Level sind in der Produktionsumgebung aktiv und ermoeglichen Angreifern Zugang zu sensiblen Systeminfos.",
- DefaultSeverity: 4,
- DefaultProbability: 2,
- ApplicableComponentTypes: []string{"software", "firmware"},
- RegulationReferences: []string{"CRA", "IEC 62443"},
- SuggestedMitigations: mustMarshalJSON([]string{"Build-Konfiguration pruefe Debug-Flag", "Produktions-Checkliste", "Debug-Port-Deaktivierung"}),
- IsBuiltin: true,
- TenantID: nil,
- CreatedAt: now,
- },
- {
- ID: hazardUUID("configuration_error", 7),
- Category: "configuration_error",
- Name: "Out-of-Bounds-Eingabe ohne Validierung",
- Description: "Nutzereingaben oder Schnittstellendaten werden ohne Bereichspruefung in sicherheitsrelevante Parameter uebernommen.",
- DefaultSeverity: 4,
- DefaultProbability: 3,
- ApplicableComponentTypes: []string{"software", "hmi"},
- RegulationReferences: []string{"IEC 62304", "Maschinenverordnung 2023/1230"},
- SuggestedMitigations: mustMarshalJSON([]string{"Eingabevalidierung", "Bereichsgrenzen definieren", "Sanity-Check"}),
- IsBuiltin: true,
- TenantID: nil,
- CreatedAt: now,
- },
- {
- ID: hazardUUID("configuration_error", 8),
- Category: "configuration_error",
- Name: "Konfigurationsdatei nicht schreibgeschuetzt",
- Description: "Sicherheitsrelevante Konfigurationsdateien koennen von unautorisierten Nutzern oder Prozessen veraendert werden.",
- DefaultSeverity: 4,
- DefaultProbability: 3,
- ApplicableComponentTypes: []string{"software", "firmware"},
- RegulationReferences: []string{"IEC 62443", "CRA"},
- SuggestedMitigations: mustMarshalJSON([]string{"Dateisystem-Berechtigungen", "Code-Signing fuer Konfig", "Integritaetspruefung"}),
- IsBuiltin: true,
- TenantID: nil,
- CreatedAt: now,
- },
-
- // ====================================================================
- // Category: safety_function_failure (8 entries)
- // ====================================================================
- {
- ID: hazardUUID("safety_function_failure", 1),
- Category: "safety_function_failure",
- Name: "Not-Halt trennt Energieversorgung nicht",
- Description: "Der Not-Halt-Taster betaetigt die Sicherheitsschalter, die Energiezufuhr wird jedoch nicht vollstaendig unterbrochen, weil das Sicherheitsrelais versagt.",
- DefaultSeverity: 5,
- DefaultProbability: 2,
- ApplicableComponentTypes: []string{"controller", "actuator"},
- RegulationReferences: []string{"Maschinenverordnung 2023/1230 Anhang I §1.2.4", "IEC 60947-5-5", "ISO 13849"},
- SuggestedMitigations: mustMarshalJSON([]string{"Regelmaessiger Not-Halt-Test", "Redundantes Sicherheitsrelais", "Selbstueberwachender Sicherheitskreis"}),
- IsBuiltin: true,
- TenantID: nil,
- CreatedAt: now,
- },
- {
- ID: hazardUUID("safety_function_failure", 2),
- Category: "safety_function_failure",
- Name: "Schutztuer-Monitoring umgangen",
- Description: "Das Schutztuer-Positionssignal wird durch einen Fehler oder Manipulation als 'geschlossen' gemeldet, obwohl die Tuer offen ist.",
- DefaultSeverity: 5,
- DefaultProbability: 2,
- ApplicableComponentTypes: []string{"sensor", "controller"},
- RegulationReferences: []string{"Maschinenverordnung 2023/1230", "ISO 14119", "ISO 13849"},
- SuggestedMitigations: mustMarshalJSON([]string{"Zwangsöffnender Positionsschalter", "Codierter Sicherheitssensor", "Anti-Tamper-Masssnahmen"}),
- IsBuiltin: true,
- TenantID: nil,
- CreatedAt: now,
- },
- {
- ID: hazardUUID("safety_function_failure", 3),
- Category: "safety_function_failure",
- Name: "Safe Speed Monitoring fehlt",
- Description: "Beim Einrichten im reduzierten Betrieb fehlt eine unabhaengige Geschwindigkeitsueberwachung, so dass der Bediener nicht ausreichend geschuetzt ist.",
- DefaultSeverity: 5,
- DefaultProbability: 2,
- ApplicableComponentTypes: []string{"controller", "software"},
- RegulationReferences: []string{"Maschinenverordnung 2023/1230", "IEC 62061", "ISO 13849"},
- SuggestedMitigations: mustMarshalJSON([]string{"Sicherheitsumrichter mit SLS", "Unabhaengige Drehzahlmessung", "SIL-2-Geschwindigkeitsueberwachung"}),
- IsBuiltin: true,
- TenantID: nil,
- CreatedAt: now,
- },
- {
- ID: hazardUUID("safety_function_failure", 4),
- Category: "safety_function_failure",
- Name: "STO-Funktion (Safe Torque Off) Fehler",
- Description: "Die STO-Sicherheitsfunktion schaltet den Antriebsmoment nicht ab, obwohl die Funktion aktiviert wurde, z.B. durch Fehler im Sicherheits-SPS-Ausgang.",
- DefaultSeverity: 5,
- DefaultProbability: 2,
- ApplicableComponentTypes: []string{"actuator", "controller"},
- RegulationReferences: []string{"IEC 61800-5-2", "Maschinenverordnung 2023/1230", "IEC 62061"},
- SuggestedMitigations: mustMarshalJSON([]string{"STO-Pruefung bei Inbetriebnahme", "Pruefzyklus im Betrieb", "Zertifizierter Sicherheitsumrichter"}),
- IsBuiltin: true,
- TenantID: nil,
- CreatedAt: now,
- },
- {
- ID: hazardUUID("safety_function_failure", 5),
- Category: "safety_function_failure",
- Name: "Muting-Missbrauch bei Lichtvorhang",
- Description: "Die Muting-Funktion des Lichtvorhangs wird durch Fehler oder Manipulation zu lange oder unkontrolliert aktiviert, was den Schutz aufhebt.",
- DefaultSeverity: 5,
- DefaultProbability: 2,
- ApplicableComponentTypes: []string{"sensor", "controller"},
- RegulationReferences: []string{"IEC 61496-3", "Maschinenverordnung 2023/1230"},
- SuggestedMitigations: mustMarshalJSON([]string{"Zeitbegrenztes Muting", "Muting-Lampe und Alarm", "Protokollierung der Muting-Ereignisse"}),
- IsBuiltin: true,
- TenantID: nil,
- CreatedAt: now,
- },
- {
- ID: hazardUUID("safety_function_failure", 6),
- Category: "safety_function_failure",
- Name: "Zweihand-Taster durch Gegenstand ueberbrueckt",
- Description: "Die Zweihand-Betaetigungseinrichtung wird durch ein eingeklemmtes Objekt permanent aktiviert, was den Bediener aus dem Schutzkonzept loest.",
- DefaultSeverity: 5,
- DefaultProbability: 2,
- ApplicableComponentTypes: []string{"hmi", "controller"},
- RegulationReferences: []string{"ISO 13851", "Maschinenverordnung 2023/1230", "ISO 13849"},
- SuggestedMitigations: mustMarshalJSON([]string{"Anti-Tie-Down-Pruefung", "Typ-III-Zweihand-Taster", "Regelmaessige Funktionskontrolle"}),
- IsBuiltin: true,
- TenantID: nil,
- CreatedAt: now,
- },
- {
- ID: hazardUUID("safety_function_failure", 7),
- Category: "safety_function_failure",
- Name: "Sicherheitsrelais-Ausfall ohne Erkennung",
- Description: "Ein Sicherheitsrelais versagt unentdeckt (z.B. verklebte Kontakte), sodass der Sicherheitskreis nicht mehr auftrennt.",
- DefaultSeverity: 5,
- DefaultProbability: 2,
- ApplicableComponentTypes: []string{"controller"},
- RegulationReferences: []string{"ISO 13849", "IEC 62061"},
- SuggestedMitigations: mustMarshalJSON([]string{"Selbstueberwachung (zwangsgefuehrt)", "Regelmaessiger Testlauf", "Redundantes Relais"}),
- IsBuiltin: true,
- TenantID: nil,
- CreatedAt: now,
- },
- {
- ID: hazardUUID("safety_function_failure", 8),
- Category: "safety_function_failure",
- Name: "Logic-Solver-Fehler in Sicherheits-SPS",
- Description: "Die Sicherheitssteuerung (Safety-SPS) fuehrt sicherheitsrelevante Logik fehlerhaft aus, z.B. durch Speicherfehler oder Prozessorfehler.",
- DefaultSeverity: 5,
- DefaultProbability: 1,
- ApplicableComponentTypes: []string{"controller", "software"},
- RegulationReferences: []string{"IEC 61511", "IEC 61508", "ISO 13849"},
- SuggestedMitigations: mustMarshalJSON([]string{"SIL-zertifizierte SPS", "Watchdog", "Selbsttest-Routinen (BIST)"}),
- IsBuiltin: true,
- TenantID: nil,
- CreatedAt: now,
- },
-
- // ====================================================================
- // Category: logging_audit_failure (5 entries)
- // ====================================================================
- {
- ID: hazardUUID("logging_audit_failure", 1),
- Category: "logging_audit_failure",
- Name: "Safety-Events nicht protokolliert",
- Description: "Sicherheitsrelevante Ereignisse (Alarme, Not-Halt-Betaetigungen, Fehlerzustaende) werden nicht in ein Protokoll geschrieben.",
- DefaultSeverity: 4,
- DefaultProbability: 3,
- ApplicableComponentTypes: []string{"software", "controller"},
- RegulationReferences: []string{"Maschinenverordnung 2023/1230", "IEC 62443", "CRA"},
- SuggestedMitigations: mustMarshalJSON([]string{"Pflicht-Logging Safety-Events", "Unveraenderliches Audit-Log", "Log-Integritaetspruefung"}),
- IsBuiltin: true,
- TenantID: nil,
- CreatedAt: now,
- },
- {
- ID: hazardUUID("logging_audit_failure", 2),
- Category: "logging_audit_failure",
- Name: "Log-Manipulation moeglich",
- Description: "Authentifizierte Benutzer oder Angreifer koennen Protokolleintraege aendern oder loeschen und so Beweise fuer Sicherheitsvorfaelle vernichten.",
- DefaultSeverity: 4,
- DefaultProbability: 2,
- ApplicableComponentTypes: []string{"software"},
- RegulationReferences: []string{"CRA", "IEC 62443"},
- SuggestedMitigations: mustMarshalJSON([]string{"Write-Once-Speicher", "Kryptografische Signaturen", "Externes Log-Management"}),
- IsBuiltin: true,
- TenantID: nil,
- CreatedAt: now,
- },
- {
- ID: hazardUUID("logging_audit_failure", 3),
- Category: "logging_audit_failure",
- Name: "Log-Overflow ueberschreibt alte Eintraege",
- Description: "Wenn der Log-Speicher voll ist, werden aeltere Eintraege ohne Warnung ueberschrieben, was eine lueckenlose Rueckverfolgung verhindert.",
- DefaultSeverity: 3,
- DefaultProbability: 4,
- ApplicableComponentTypes: []string{"software", "controller"},
- RegulationReferences: []string{"CRA", "IEC 62443"},
- SuggestedMitigations: mustMarshalJSON([]string{"Log-Kapazitaetsalarm", "Externes Log-System", "Zirkulaerpuffer mit Warnschwelle"}),
- IsBuiltin: true,
- TenantID: nil,
- CreatedAt: now,
- },
- {
- ID: hazardUUID("logging_audit_failure", 4),
- Category: "logging_audit_failure",
- Name: "Fehlende Zeitstempel in Protokolleintraegen",
- Description: "Log-Eintraege enthalten keine oder ungenaue Zeitstempel, was die zeitliche Rekonstruktion von Ereignissen bei der Fehlersuche verhindert.",
- DefaultSeverity: 3,
- DefaultProbability: 3,
- ApplicableComponentTypes: []string{"software", "controller"},
- RegulationReferences: []string{"CRA", "Maschinenverordnung 2023/1230"},
- SuggestedMitigations: mustMarshalJSON([]string{"NTP-Synchronisation", "RTC im Geraet", "ISO-8601-Zeitstempel"}),
- IsBuiltin: true,
- TenantID: nil,
- CreatedAt: now,
- },
- {
- ID: hazardUUID("logging_audit_failure", 5),
- Category: "logging_audit_failure",
- Name: "Audit-Trail loeschbar durch Bediener",
- Description: "Der Audit-Trail kann von einem normalen Bediener geloescht werden, was die Nachvollziehbarkeit von Sicherheitsereignissen untergaebt.",
- DefaultSeverity: 4,
- DefaultProbability: 2,
- ApplicableComponentTypes: []string{"software"},
- RegulationReferences: []string{"CRA", "IEC 62443", "Maschinenverordnung 2023/1230"},
- SuggestedMitigations: mustMarshalJSON([]string{"RBAC: Nur Admin darf loeschen", "Log-Export vor Loeschung", "Unanderbare Log-Speicherung"}),
- IsBuiltin: true,
- TenantID: nil,
- CreatedAt: now,
- },
-
- // ====================================================================
- // Category: integration_error (8 entries)
- // ====================================================================
- {
- ID: hazardUUID("integration_error", 1),
- Category: "integration_error",
- Name: "Datentyp-Mismatch an Schnittstelle",
- Description: "Zwei Systeme tauschen Daten ueber eine Schnittstelle aus, die inkompatible Datentypen verwendet, was zu Interpretationsfehlern fuehrt.",
- DefaultSeverity: 4,
- DefaultProbability: 3,
- ApplicableComponentTypes: []string{"software", "network"},
- RegulationReferences: []string{"IEC 62304", "IEC 62443"},
- SuggestedMitigations: mustMarshalJSON([]string{"Schnittstellendefinition (IDL/Protobuf)", "Integrationstests", "Datentypvalidierung"}),
- IsBuiltin: true,
- TenantID: nil,
- CreatedAt: now,
- },
- {
- ID: hazardUUID("integration_error", 2),
- Category: "integration_error",
- Name: "Endianness-Fehler bei Datenuebertragung",
- Description: "Big-Endian- und Little-Endian-Systeme kommunizieren ohne Byte-Order-Konvertierung, was zu falsch interpretierten numerischen Werten fuehrt.",
- DefaultSeverity: 4,
- DefaultProbability: 3,
- ApplicableComponentTypes: []string{"software", "network"},
- RegulationReferences: []string{"IEC 62304", "IEC 62443"},
- SuggestedMitigations: mustMarshalJSON([]string{"Explizite Byte-Order-Definiton", "Integrationstests", "Schnittstellenspezifikation"}),
- IsBuiltin: true,
- TenantID: nil,
- CreatedAt: now,
- },
- {
- ID: hazardUUID("integration_error", 3),
- Category: "integration_error",
- Name: "Protokoll-Versions-Konflikt",
- Description: "Sender und Empfaenger verwenden unterschiedliche Protokollversionen, die nicht rueckwaertskompatibel sind, was zu Paketablehnung oder Fehlinterpretation fuehrt.",
- DefaultSeverity: 4,
- DefaultProbability: 3,
- ApplicableComponentTypes: []string{"software", "network", "firmware"},
- RegulationReferences: []string{"IEC 62443", "CRA"},
- SuggestedMitigations: mustMarshalJSON([]string{"Versions-Aushandlung beim Verbindungsaufbau", "Backward-Compatibilitaet", "Kompatibilitaets-Matrix"}),
- IsBuiltin: true,
- TenantID: nil,
- CreatedAt: now,
- },
- {
- ID: hazardUUID("integration_error", 4),
- Category: "integration_error",
- Name: "Timeout nicht behandelt bei Kommunikation",
- Description: "Eine Kommunikationsverbindung bricht ab oder antwortet nicht, der Sender erkennt dies nicht und wartet unendlich lang.",
- DefaultSeverity: 4,
- DefaultProbability: 3,
- ApplicableComponentTypes: []string{"software", "network"},
- RegulationReferences: []string{"IEC 62443", "Maschinenverordnung 2023/1230"},
- SuggestedMitigations: mustMarshalJSON([]string{"Timeout-Konfiguration", "Watchdog-Timer", "Fail-Safe bei Verbindungsverlust"}),
- IsBuiltin: true,
- TenantID: nil,
- CreatedAt: now,
- },
- {
- ID: hazardUUID("integration_error", 5),
- Category: "integration_error",
- Name: "Buffer Overflow an Schnittstelle",
- Description: "Eine Schnittstelle akzeptiert Eingaben, die groesser als der zugewiesene Puffer sind, was zu Speicher-Ueberschreibung und Kontrollfluss-Manipulation fuehrt.",
- DefaultSeverity: 5,
- DefaultProbability: 2,
- ApplicableComponentTypes: []string{"software", "firmware", "network"},
- RegulationReferences: []string{"CRA", "IEC 62443", "IEC 62304"},
- SuggestedMitigations: mustMarshalJSON([]string{"Laengenvalidierung", "Sichere Puffer-Funktionen", "Statische Analyse (z.B. MISRA)"}),
- IsBuiltin: true,
- TenantID: nil,
- CreatedAt: now,
- },
- {
- ID: hazardUUID("integration_error", 6),
- Category: "integration_error",
- Name: "Fehlender Heartbeat bei Safety-Verbindung",
- Description: "Eine Safety-Kommunikationsverbindung sendet keinen periodischen Heartbeat, so dass ein stiller Ausfall (z.B. unterbrochenes Kabel) nicht erkannt wird.",
- DefaultSeverity: 5,
- DefaultProbability: 2,
- ApplicableComponentTypes: []string{"network", "software"},
- RegulationReferences: []string{"IEC 61784-3", "ISO 13849", "IEC 62061"},
- SuggestedMitigations: mustMarshalJSON([]string{"Heartbeat-Protokoll", "Verbindungsueberwachung", "Safe-State bei Heartbeat-Ausfall"}),
- IsBuiltin: true,
- TenantID: nil,
- CreatedAt: now,
- },
- {
- ID: hazardUUID("integration_error", 7),
- Category: "integration_error",
- Name: "Falscher Skalierungsfaktor bei Sensordaten",
- Description: "Sensordaten werden mit einem falschen Faktor skaliert, was zu signifikant fehlerhaften Messwerten und moeglichen Fehlentscheidungen fuehrt.",
- DefaultSeverity: 4,
- DefaultProbability: 3,
- ApplicableComponentTypes: []string{"sensor", "software"},
- RegulationReferences: []string{"Maschinenverordnung 2023/1230", "IEC 62304"},
- SuggestedMitigations: mustMarshalJSON([]string{"Kalibrierungspruefung", "Plausibilitaetstest", "Schnittstellendokumentation"}),
- IsBuiltin: true,
- TenantID: nil,
- CreatedAt: now,
- },
- {
- ID: hazardUUID("integration_error", 8),
- Category: "integration_error",
- Name: "Einheitenfehler (mm vs. inch)",
- Description: "Unterschiedliche Masseinheiten zwischen Systemen fuehren zu fehlerhaften Bewegungsbefehlen oder Werkzeugpositionierungen.",
- DefaultSeverity: 4,
- DefaultProbability: 3,
- ApplicableComponentTypes: []string{"software", "hmi"},
- RegulationReferences: []string{"Maschinenverordnung 2023/1230", "IEC 62304"},
- SuggestedMitigations: mustMarshalJSON([]string{"Explizite Einheitendefinition", "Einheitenkonvertierung in der Schnittstelle", "Integrationstests"}),
- IsBuiltin: true,
- TenantID: nil,
- CreatedAt: now,
- },
-
- // ====================================================================
- // Category: environmental_hazard (5 entries)
- // ====================================================================
- {
- ID: hazardUUID("environmental_hazard", 1),
- Category: "environmental_hazard",
- Name: "Ausfall durch hohe Umgebungstemperatur",
- Description: "Hohe Umgebungstemperaturen ueberschreiten die spezifizierten Grenzwerte der Elektronik oder Aktorik und fuehren zu Fehlfunktionen.",
- DefaultSeverity: 4,
- DefaultProbability: 3,
- ApplicableComponentTypes: []string{"controller", "sensor"},
- RegulationReferences: []string{"Maschinenverordnung 2023/1230", "IEC 60068-2"},
- SuggestedMitigations: mustMarshalJSON([]string{"Betriebstemperatur-Spezifikation einhalten", "Klimaanlagensystem", "Temperatursensor + Abschaltung"}),
- IsBuiltin: true,
- TenantID: nil,
- CreatedAt: now,
- },
- {
- ID: hazardUUID("environmental_hazard", 2),
- Category: "environmental_hazard",
- Name: "Ausfall bei Tieftemperatur",
- Description: "Sehr tiefe Temperaturen reduzieren die Viskositaet von Hydraulikfluessigkeiten, beeinflussen Elektronik und fuehren zu mechanischen Ausfaellen.",
- DefaultSeverity: 4,
- DefaultProbability: 3,
- ApplicableComponentTypes: []string{"actuator", "controller"},
- RegulationReferences: []string{"Maschinenverordnung 2023/1230", "IEC 60068-2"},
- SuggestedMitigations: mustMarshalJSON([]string{"Tieftemperatur-spezifizierte Komponenten", "Heizung im Schaltschrank", "Anlaeufroutine bei Kaeltestart"}),
- IsBuiltin: true,
- TenantID: nil,
- CreatedAt: now,
- },
- {
- ID: hazardUUID("environmental_hazard", 3),
- Category: "environmental_hazard",
- Name: "Korrosion durch Feuchtigkeit",
- Description: "Hohe Luftfeuchtigkeit oder Kondenswasser fuehrt zur Korrosion von Kontakten und Leiterbahnen, was zu Ausfaellen und Isolationsfehlern fuehrt.",
- DefaultSeverity: 3,
- DefaultProbability: 4,
- ApplicableComponentTypes: []string{"controller", "sensor"},
- RegulationReferences: []string{"Maschinenverordnung 2023/1230", "IEC 60529"},
- SuggestedMitigations: mustMarshalJSON([]string{"IP-Schutz entsprechend der Umgebung", "Belueftung mit Filter", "Regelmaessige Inspektion"}),
- IsBuiltin: true,
- TenantID: nil,
- CreatedAt: now,
- },
- {
- ID: hazardUUID("environmental_hazard", 4),
- Category: "environmental_hazard",
- Name: "Fehlfunktion durch Vibrationen",
- Description: "Mechanische Vibrationen lockern Verbindungen, schuetteln Kontakte auf oder beschaedigen Loetpunkte in Elektronikbaugruppen.",
- DefaultSeverity: 4,
- DefaultProbability: 3,
- ApplicableComponentTypes: []string{"controller", "sensor"},
- RegulationReferences: []string{"Maschinenverordnung 2023/1230", "IEC 60068-2-6"},
- SuggestedMitigations: mustMarshalJSON([]string{"Vibrationsdaempfung", "Vergossene Elektronik", "Regelmaessige Verbindungskontrolle"}),
- IsBuiltin: true,
- TenantID: nil,
- CreatedAt: now,
- },
- {
- ID: hazardUUID("environmental_hazard", 5),
- Category: "environmental_hazard",
- Name: "Kontamination durch Staub oder Fluessigkeiten",
- Description: "Staub, Metallspaeene oder Kuehlmittel gelangen in das Gehaeuseinnere und fuehren zu Kurzschluessen, Isolationsfehlern oder Kuehlproblemen.",
- DefaultSeverity: 3,
- DefaultProbability: 4,
- ApplicableComponentTypes: []string{"controller", "hmi"},
- RegulationReferences: []string{"Maschinenverordnung 2023/1230", "IEC 60529"},
- SuggestedMitigations: mustMarshalJSON([]string{"Hohe IP-Schutzklasse", "Dichtungen regelmaessig pruefen", "Ueberdruck im Schaltschrank"}),
- IsBuiltin: true,
- TenantID: nil,
- CreatedAt: now,
- },
-
- // ====================================================================
- // Category: maintenance_hazard (6 entries)
- // ====================================================================
- {
- ID: hazardUUID("maintenance_hazard", 1),
- Category: "maintenance_hazard",
- Name: "Wartung ohne LOTO-Prozedur",
- Description: "Wartungsarbeiten werden ohne korrekte Lockout/Tagout-Prozedur durchgefuehrt, sodass die Maschine waehrend der Arbeit anlaufen kann.",
- DefaultSeverity: 5,
- DefaultProbability: 3,
- ApplicableComponentTypes: []string{"controller", "software"},
- RegulationReferences: []string{"Maschinenverordnung 2023/1230 Anhang I §1.6.3"},
- SuggestedMitigations: mustMarshalJSON([]string{"LOTO-Funktion in Software", "Schulung", "Prozedur im Betriebshandbuch"}),
- IsBuiltin: true,
- TenantID: nil,
- CreatedAt: now,
- },
- {
- ID: hazardUUID("maintenance_hazard", 2),
- Category: "maintenance_hazard",
- Name: "Fehlende LOTO-Funktion in Software",
- Description: "Die Steuerungssoftware bietet keine Moeglichkeit, die Maschine fuer Wartungsarbeiten sicher zu sperren und zu verriegeln.",
- DefaultSeverity: 5,
- DefaultProbability: 2,
- ApplicableComponentTypes: []string{"software", "hmi"},
- RegulationReferences: []string{"Maschinenverordnung 2023/1230 Anhang I §1.6.3"},
- SuggestedMitigations: mustMarshalJSON([]string{"Software-LOTO implementieren", "Wartungsmodus mit Schluessel", "Energiesperrfunktion"}),
- IsBuiltin: true,
- TenantID: nil,
- CreatedAt: now,
- },
- {
- ID: hazardUUID("maintenance_hazard", 3),
- Category: "maintenance_hazard",
- Name: "Wartung bei laufender Maschine",
- Description: "Wartungsarbeiten werden an betriebener Maschine durchgefuehrt, weil kein erzwungener Wartungsmodus vorhanden ist.",
- DefaultSeverity: 5,
- DefaultProbability: 2,
- ApplicableComponentTypes: []string{"software", "controller"},
- RegulationReferences: []string{"Maschinenverordnung 2023/1230", "ISO 13849"},
- SuggestedMitigations: mustMarshalJSON([]string{"Erzwungenes Abschalten fuer Wartungsmodus", "Schluesselschalter", "Schutzmassnahmen im Wartungsmodus"}),
- IsBuiltin: true,
- TenantID: nil,
- CreatedAt: now,
- },
- {
- ID: hazardUUID("maintenance_hazard", 4),
- Category: "maintenance_hazard",
- Name: "Wartungs-Tool ohne Zugangskontrolle",
- Description: "Ein Diagnose- oder Wartungswerkzeug ist ohne Authentifizierung zugaenglich und ermoeglicht die unbeaufsichtigte Aenderung von Sicherheitsparametern.",
- DefaultSeverity: 4,
- DefaultProbability: 2,
- ApplicableComponentTypes: []string{"software", "hmi"},
- RegulationReferences: []string{"IEC 62443", "CRA"},
- SuggestedMitigations: mustMarshalJSON([]string{"Authentifizierung fuer Wartungs-Tools", "Rollenkonzept", "Audit-Log fuer Wartungszugriffe"}),
- IsBuiltin: true,
- TenantID: nil,
- CreatedAt: now,
- },
- {
- ID: hazardUUID("maintenance_hazard", 5),
- Category: "maintenance_hazard",
- Name: "Unsichere Demontage gefaehrlicher Baugruppen",
- Description: "Die Betriebsanleitung beschreibt nicht, wie gefaehrliche Baugruppen (z.B. Hochvolt, gespeicherte Energie) sicher demontiert werden.",
- DefaultSeverity: 5,
- DefaultProbability: 2,
- ApplicableComponentTypes: []string{"other"},
- RegulationReferences: []string{"Maschinenverordnung 2023/1230 Anhang I §1.7.4"},
- SuggestedMitigations: mustMarshalJSON([]string{"Detaillierte Demontageanleitung", "Warnhinweise an Geraet", "Schulung des Wartungspersonals"}),
- IsBuiltin: true,
- TenantID: nil,
- CreatedAt: now,
- },
- {
- ID: hazardUUID("maintenance_hazard", 6),
- Category: "maintenance_hazard",
- Name: "Wiederanlauf nach Wartung ohne Freigabeprozedur",
- Description: "Nach Wartungsarbeiten wird die Maschine ohne formelle Freigabeprozedur wieder in Betrieb genommen, was zu Verletzungen bei noch anwesendem Personal fuehren kann.",
- DefaultSeverity: 5,
- DefaultProbability: 2,
- ApplicableComponentTypes: []string{"software", "hmi"},
- RegulationReferences: []string{"Maschinenverordnung 2023/1230 Anhang I §1.6.3", "ISO 13849"},
- SuggestedMitigations: mustMarshalJSON([]string{"Software-Wiederanlauf-Freigabe", "Gefahrenbereich-Pruefung vor Anlauf", "Akustisches Warnsignal vor Anlauf"}),
- IsBuiltin: true,
- TenantID: nil,
- CreatedAt: now,
- },
- }
-
- entries = append(entries, extended...)
-
- // ====================================================================
- // ISO 12100 Machine Safety Hazard Extensions (~54 entries)
- // ====================================================================
-
- iso12100Entries := []HazardLibraryEntry{
- // ====================================================================
- // Category: mechanical_hazard (indices 7-20, 14 entries)
- // ====================================================================
- {
- ID: hazardUUID("mechanical_hazard", 7),
- Category: "mechanical_hazard",
- SubCategory: "quetschgefahr",
- Name: "Quetschgefahr durch gegenlaeufige Walzen",
- Description: "Zwischen gegenlaeufig rotierenden Walzen entsteht ein Einzugspunkt, an dem Koerperteile oder Kleidung eingezogen und gequetscht werden koennen.",
- DefaultSeverity: 5,
- DefaultProbability: 3,
- DefaultExposure: 3,
- DefaultAvoidance: 3,
- ApplicableComponentTypes: []string{"mechanical", "actuator"},
- RegulationReferences: []string{"Maschinenverordnung 2023/1230 Anhang I"},
- SuggestedMitigations: mustMarshalJSON([]string{"Feststehende trennende Schutzeinrichtung am Walzeneinlauf", "Zweihandbedienung bei manueller Beschickung"}),
- TypicalCauses: []string{"Fehlende Schutzabdeckung am Einzugspunkt", "Manuelle Materialzufuehrung ohne Hilfsmittel", "Wartung bei laufender Maschine"},
- TypicalHarm: "Quetschverletzungen an Fingern, Haenden oder Armen bis hin zu Amputationen",
- RelevantLifecyclePhases: []string{"normal_operation", "maintenance", "setup"},
- RecommendedMeasuresDesign: []string{"Mindestabstand zwischen Walzen groesser als 25 mm oder kleiner als 5 mm", "Einzugspunkt ausserhalb der Reichweite positionieren"},
- RecommendedMeasuresTechnical: []string{"Schutzgitter mit Sicherheitsverriegelung", "Lichtschranke vor dem Einzugsbereich"},
- RecommendedMeasuresInformation: []string{"Warnschilder am Einzugspunkt", "Betriebsanweisung zur sicheren Beschickung"},
- SuggestedEvidence: []string{"Pruefbericht der Schutzeinrichtung", "Risikobeurteilung nach ISO 12100"},
- RelatedKeywords: []string{"Walzen", "Einzugspunkt", "Quetschstelle"},
- IsBuiltin: true,
- TenantID: nil,
- CreatedAt: now,
- },
- {
- ID: hazardUUID("mechanical_hazard", 8),
- Category: "mechanical_hazard",
- SubCategory: "schergefahr",
- Name: "Schergefahr an beweglichen Maschinenteilen",
- Description: "Durch gegeneinander bewegte Maschinenteile entstehen Scherstellen, die zu schweren Schnitt- und Trennverletzungen fuehren koennen.",
- DefaultSeverity: 5,
- DefaultProbability: 3,
- DefaultExposure: 3,
- DefaultAvoidance: 3,
- ApplicableComponentTypes: []string{"mechanical", "actuator"},
- RegulationReferences: []string{"Maschinenverordnung 2023/1230 Anhang I"},
- SuggestedMitigations: mustMarshalJSON([]string{"Trennende Schutzeinrichtung an der Scherstelle", "Sicherheitsabstand nach ISO 13857"}),
- TypicalCauses: []string{"Unzureichender Sicherheitsabstand", "Fehlende Schutzverkleidung", "Eingriff waehrend des Betriebs"},
- TypicalHarm: "Schnitt- und Trennverletzungen an Fingern und Haenden",
- RelevantLifecyclePhases: []string{"normal_operation", "maintenance"},
- RecommendedMeasuresDesign: []string{"Sicherheitsabstaende nach ISO 13857 einhalten", "Scherstellen konstruktiv vermeiden"},
- RecommendedMeasuresTechnical: []string{"Verriegelte Schutzhauben", "Not-Halt in unmittelbarer Naehe"},
- RecommendedMeasuresInformation: []string{"Gefahrenhinweis an Scherstellen", "Schulung der Bediener"},
- SuggestedEvidence: []string{"Abstandsmessung gemaess ISO 13857", "Risikobeurteilung"},
- RelatedKeywords: []string{"Scherstelle", "Gegenlaeufig", "Schneidgefahr"},
- IsBuiltin: true,
- TenantID: nil,
- CreatedAt: now,
- },
- {
- ID: hazardUUID("mechanical_hazard", 9),
- Category: "mechanical_hazard",
- SubCategory: "schneidgefahr",
- Name: "Schneidgefahr durch rotierende Werkzeuge",
- Description: "Rotierende Schneidwerkzeuge wie Fraeser, Saegeblaetter oder Messer koennen bei Kontakt schwere Schnittverletzungen verursachen.",
- DefaultSeverity: 5,
- DefaultProbability: 3,
- DefaultExposure: 3,
- DefaultAvoidance: 2,
- ApplicableComponentTypes: []string{"mechanical"},
- RegulationReferences: []string{"Maschinenverordnung 2023/1230 Anhang I"},
- SuggestedMitigations: mustMarshalJSON([]string{"Vollstaendige Einhausung des Werkzeugs", "Automatische Werkzeugbremse bei Schutztueroeffnung"}),
- TypicalCauses: []string{"Offene Schutzhaube waehrend des Betriebs", "Nachlauf des Werkzeugs nach Abschaltung", "Werkzeugbruch"},
- TypicalHarm: "Tiefe Schnittwunden bis hin zu Gliedmassentrennung",
- RelevantLifecyclePhases: []string{"normal_operation", "setup", "maintenance"},
- RecommendedMeasuresDesign: []string{"Vollstaendige Einhausung mit Verriegelung", "Werkzeugbremse mit kurzer Nachlaufzeit"},
- RecommendedMeasuresTechnical: []string{"Verriegelte Schutzhaube mit Zuhaltung", "Drehzahlueberwachung"},
- RecommendedMeasuresInformation: []string{"Warnhinweis zur Nachlaufzeit", "Betriebsanweisung zum Werkzeugwechsel"},
- SuggestedEvidence: []string{"Nachlaufzeitmessung", "Pruefbericht Schutzeinrichtung"},
- RelatedKeywords: []string{"Fraeser", "Saegeblatt", "Schneidwerkzeug"},
- IsBuiltin: true,
- TenantID: nil,
- CreatedAt: now,
- },
- {
- ID: hazardUUID("mechanical_hazard", 10),
- Category: "mechanical_hazard",
- SubCategory: "einzugsgefahr",
- Name: "Einzugsgefahr durch Foerderbaender",
- Description: "An Umlenkrollen und Antriebstrommeln von Foerderbaendern bestehen Einzugsstellen, die Koerperteile oder Kleidung erfassen koennen.",
- DefaultSeverity: 4,
- DefaultProbability: 3,
- DefaultExposure: 4,
- DefaultAvoidance: 3,
- ApplicableComponentTypes: []string{"mechanical", "actuator"},
- RegulationReferences: []string{"Maschinenverordnung 2023/1230 Anhang I"},
- SuggestedMitigations: mustMarshalJSON([]string{"Schutzverkleidung an Umlenkrollen", "Not-Halt-Reissleine entlang des Foerderbands"}),
- TypicalCauses: []string{"Fehlende Abdeckung an Umlenkpunkten", "Reinigung bei laufendem Band", "Lose Kleidung des Personals"},
- TypicalHarm: "Einzugsverletzungen an Armen und Haenden, Quetschungen",
- RelevantLifecyclePhases: []string{"normal_operation", "cleaning", "maintenance"},
- RecommendedMeasuresDesign: []string{"Umlenkrollen mit Schutzverkleidung", "Unterflur-Foerderung wo moeglich"},
- RecommendedMeasuresTechnical: []string{"Not-Halt-Reissleine", "Bandschieflauf-Erkennung"},
- RecommendedMeasuresInformation: []string{"Kleidervorschrift fuer Bedienpersonal", "Sicherheitsunterweisung"},
- SuggestedEvidence: []string{"Pruefbericht der Schutzeinrichtungen", "Risikobeurteilung"},
- RelatedKeywords: []string{"Foerderband", "Umlenkrolle", "Einzugsstelle"},
- IsBuiltin: true,
- TenantID: nil,
- CreatedAt: now,
- },
- {
- ID: hazardUUID("mechanical_hazard", 11),
- Category: "mechanical_hazard",
- SubCategory: "erfassungsgefahr",
- Name: "Erfassungsgefahr durch rotierende Wellen",
- Description: "Freiliegende rotierende Wellen, Kupplungen oder Zapfen koennen Kleidung oder Haare erfassen und Personen in die Drehbewegung hineinziehen.",
- DefaultSeverity: 5,
- DefaultProbability: 3,
- DefaultExposure: 3,
- DefaultAvoidance: 2,
- ApplicableComponentTypes: []string{"mechanical", "actuator"},
- RegulationReferences: []string{"Maschinenverordnung 2023/1230 Anhang I"},
- SuggestedMitigations: mustMarshalJSON([]string{"Vollstaendige Verkleidung rotierender Wellen", "Drehmomentbegrenzung"}),
- TypicalCauses: []string{"Fehlende Wellenabdeckung", "Lose Kleidungsstuecke", "Wartung bei laufender Welle"},
- TypicalHarm: "Erfassungsverletzungen mit Knochenbruechen, Skalpierungen oder toedlichem Ausgang",
- RelevantLifecyclePhases: []string{"normal_operation", "maintenance"},
- RecommendedMeasuresDesign: []string{"Wellen vollstaendig einhausen", "Kupplungen mit Schutzhuelsen"},
- RecommendedMeasuresTechnical: []string{"Verriegelte Schutzabdeckung", "Stillstandsueberwachung fuer Wartungszugang"},
- RecommendedMeasuresInformation: []string{"Kleiderordnung ohne lose Teile", "Warnschilder an Wellenabdeckungen"},
- SuggestedEvidence: []string{"Inspektionsbericht Wellenabdeckungen", "Risikobeurteilung"},
- RelatedKeywords: []string{"Welle", "Kupplung", "Erfassung"},
- IsBuiltin: true,
- TenantID: nil,
- CreatedAt: now,
- },
- {
- ID: hazardUUID("mechanical_hazard", 12),
- Category: "mechanical_hazard",
- SubCategory: "stossgefahr",
- Name: "Stossgefahr durch pneumatische/hydraulische Zylinder",
- Description: "Schnell ausfahrende Pneumatik- oder Hydraulikzylinder koennen Personen stossen oder einklemmen, insbesondere bei unerwartetem Anlauf.",
- DefaultSeverity: 4,
- DefaultProbability: 3,
- DefaultExposure: 3,
- DefaultAvoidance: 3,
- ApplicableComponentTypes: []string{"actuator", "mechanical"},
- RegulationReferences: []string{"Maschinenverordnung 2023/1230 Anhang I"},
- SuggestedMitigations: mustMarshalJSON([]string{"Geschwindigkeitsbegrenzung durch Drosselventile", "Schutzeinrichtung im Bewegungsbereich"}),
- TypicalCauses: []string{"Fehlende Endlagendaempfung", "Unerwarteter Druckaufbau", "Aufenthalt im Bewegungsbereich"},
- TypicalHarm: "Prellungen, Knochenbrueche, Einklemmverletzungen",
- RelevantLifecyclePhases: []string{"normal_operation", "setup", "maintenance"},
- RecommendedMeasuresDesign: []string{"Endlagendaempfung vorsehen", "Zylindergeschwindigkeit begrenzen"},
- RecommendedMeasuresTechnical: []string{"Lichtvorhang im Bewegungsbereich", "Druckspeicher-Entlastungsventil"},
- RecommendedMeasuresInformation: []string{"Kennzeichnung des Bewegungsbereichs", "Betriebsanweisung"},
- SuggestedEvidence: []string{"Geschwindigkeitsmessung", "Risikobeurteilung"},
- RelatedKeywords: []string{"Zylinder", "Pneumatik", "Stossgefahr"},
- IsBuiltin: true,
- TenantID: nil,
- CreatedAt: now,
- },
- {
- ID: hazardUUID("mechanical_hazard", 13),
- Category: "mechanical_hazard",
- SubCategory: "herabfallende_teile",
- Name: "Herabfallende Teile aus Werkstueckhalterung",
- Description: "Unzureichend gesicherte Werkstuecke oder Werkzeuge koennen sich aus der Halterung loesen und herabfallen.",
- DefaultSeverity: 4,
- DefaultProbability: 2,
- DefaultExposure: 3,
- DefaultAvoidance: 3,
- ApplicableComponentTypes: []string{"mechanical"},
- RegulationReferences: []string{"Maschinenverordnung 2023/1230 Anhang I"},
- SuggestedMitigations: mustMarshalJSON([]string{"Spannkraftueberwachung der Halterung", "Schutzdach ueber dem Bedienerbereich"}),
- TypicalCauses: []string{"Unzureichende Spannkraft", "Vibration lockert die Halterung", "Falsches Werkstueck-Spannmittel"},
- TypicalHarm: "Kopfverletzungen, Prellungen, Quetschungen durch herabfallende Teile",
- RelevantLifecyclePhases: []string{"normal_operation", "setup"},
- RecommendedMeasuresDesign: []string{"Spannkraftueberwachung mit Abschaltung", "Auffangvorrichtung unter Werkstueck"},
- RecommendedMeasuresTechnical: []string{"Sensor zur Spannkraftueberwachung", "Schutzhaube"},
- RecommendedMeasuresInformation: []string{"Pruefanweisung vor Bearbeitungsstart", "Schutzhelmpflicht im Gefahrenbereich"},
- SuggestedEvidence: []string{"Pruefprotokoll Spannmittel", "Risikobeurteilung"},
- RelatedKeywords: []string{"Werkstueck", "Spannmittel", "Herabfallen"},
- IsBuiltin: true,
- TenantID: nil,
- CreatedAt: now,
- },
- {
- ID: hazardUUID("mechanical_hazard", 14),
- Category: "mechanical_hazard",
- SubCategory: "wegschleudern",
- Name: "Wegschleudern von Bruchstuecken bei Werkzeugversagen",
- Description: "Bei Werkzeugbruch koennen Bruchstuecke mit hoher Geschwindigkeit weggeschleudert werden und Personen im Umfeld verletzen.",
- DefaultSeverity: 5,
- DefaultProbability: 2,
- DefaultExposure: 3,
- DefaultAvoidance: 2,
- ApplicableComponentTypes: []string{"mechanical"},
- RegulationReferences: []string{"Maschinenverordnung 2023/1230 Anhang I"},
- SuggestedMitigations: mustMarshalJSON([]string{"Splitterschutzscheibe aus Polycarbonat", "Regelmaessige Werkzeuginspektion"}),
- TypicalCauses: []string{"Werkzeugverschleiss", "Ueberschreitung der zulaessigen Drehzahl", "Materialfehler im Werkzeug"},
- TypicalHarm: "Durchdringende Verletzungen durch Bruchstuecke, Augenverletzungen",
- RelevantLifecyclePhases: []string{"normal_operation"},
- RecommendedMeasuresDesign: []string{"Splitterschutz in der Einhausung", "Drehzahlbegrenzung des Werkzeugs"},
- RecommendedMeasuresTechnical: []string{"Unwuchtueberwachung", "Brucherkennungssensor"},
- RecommendedMeasuresInformation: []string{"Maximaldrehzahl am Werkzeug kennzeichnen", "Schutzbrillenpflicht"},
- SuggestedEvidence: []string{"Bersttest der Einhausung", "Werkzeuginspektionsprotokoll"},
- RelatedKeywords: []string{"Werkzeugbruch", "Splitter", "Schleudern"},
- IsBuiltin: true,
- TenantID: nil,
- CreatedAt: now,
- },
- {
- ID: hazardUUID("mechanical_hazard", 15),
- Category: "mechanical_hazard",
- SubCategory: "instabilitaet",
- Name: "Instabilitaet der Maschine durch fehlendes Fundament",
- Description: "Eine unzureichend verankerte oder falsch aufgestellte Maschine kann kippen oder sich verschieben, insbesondere bei dynamischen Kraeften.",
- DefaultSeverity: 4,
- DefaultProbability: 2,
- DefaultExposure: 2,
- DefaultAvoidance: 3,
- ApplicableComponentTypes: []string{"mechanical"},
- RegulationReferences: []string{"Maschinenverordnung 2023/1230 Anhang I"},
- SuggestedMitigations: mustMarshalJSON([]string{"Fundamentberechnung und Verankerung", "Standsicherheitsnachweis"}),
- TypicalCauses: []string{"Fehlende Bodenverankerung", "Ungeeigneter Untergrund", "Erhoehte dynamische Lasten"},
- TypicalHarm: "Quetschverletzungen durch kippende Maschine, Sachschaeden",
- RelevantLifecyclePhases: []string{"installation", "normal_operation", "transport"},
- RecommendedMeasuresDesign: []string{"Niedriger Schwerpunkt der Maschine", "Befestigungspunkte im Maschinenrahmen"},
- RecommendedMeasuresTechnical: []string{"Bodenverankerung mit Schwerlastduebeln", "Nivellierelemente mit Kippsicherung"},
- RecommendedMeasuresInformation: []string{"Aufstellanleitung mit Fundamentplan", "Hinweis auf maximale Bodenbelastung"},
- SuggestedEvidence: []string{"Standsicherheitsnachweis", "Fundamentplan"},
- RelatedKeywords: []string{"Fundament", "Standsicherheit", "Kippen"},
- IsBuiltin: true,
- TenantID: nil,
- CreatedAt: now,
- },
- {
- ID: hazardUUID("mechanical_hazard", 16),
- Category: "mechanical_hazard",
- SubCategory: "wiederanlauf",
- Name: "Unkontrollierter Wiederanlauf nach Energieunterbruch",
- Description: "Nach einem Stromausfall oder Druckabfall kann die Maschine unkontrolliert wieder anlaufen und Personen im Gefahrenbereich verletzen.",
- DefaultSeverity: 5,
- DefaultProbability: 3,
- DefaultExposure: 3,
- DefaultAvoidance: 2,
- ApplicableComponentTypes: []string{"mechanical", "controller", "electrical"},
- RegulationReferences: []string{"Maschinenverordnung 2023/1230 Anhang I"},
- SuggestedMitigations: mustMarshalJSON([]string{"Wiederanlaufsperre nach Energierueckkehr", "Quittierungspflichtiger Neustart"}),
- TypicalCauses: []string{"Fehlende Wiederanlaufsperre", "Stromausfall mit anschliessendem automatischem Neustart", "Druckaufbau nach Leckagereparatur"},
- TypicalHarm: "Verletzungen durch unerwartete Maschinenbewegung bei Wiederanlauf",
- RelevantLifecyclePhases: []string{"normal_operation", "maintenance", "fault_finding"},
- RecommendedMeasuresDesign: []string{"Wiederanlaufsperre in der Steuerung", "Energiespeicher sicher entladen"},
- RecommendedMeasuresTechnical: []string{"Schaltschuetz mit Selbsthaltung", "Druckschalter mit Ruecksetzbedingung"},
- RecommendedMeasuresInformation: []string{"Hinweis auf Wiederanlaufverhalten", "Verfahrensanweisung nach Energieausfall"},
- SuggestedEvidence: []string{"Funktionstest Wiederanlaufsperre", "Risikobeurteilung"},
- RelatedKeywords: []string{"Wiederanlauf", "Stromausfall", "Anlaufsperre"},
- IsBuiltin: true,
- TenantID: nil,
- CreatedAt: now,
- },
- {
- ID: hazardUUID("mechanical_hazard", 17),
- Category: "mechanical_hazard",
- SubCategory: "reibungsgefahr",
- Name: "Reibungsgefahr an rauen Oberflaechen",
- Description: "Raue, scharfkantige oder gratbehaftete Maschinenoberlaechen koennen bei Kontakt zu Hautabschuerfungen und Schnittverletzungen fuehren.",
- DefaultSeverity: 3,
- DefaultProbability: 3,
- DefaultExposure: 4,
- DefaultAvoidance: 4,
- ApplicableComponentTypes: []string{"mechanical"},
- RegulationReferences: []string{"Maschinenverordnung 2023/1230 Anhang I"},
- SuggestedMitigations: mustMarshalJSON([]string{"Entgraten aller zugaenglichen Kanten", "Schutzhandschuhe fuer Bedienpersonal"}),
- TypicalCauses: []string{"Nicht entgratete Schnittkanten", "Korrosionsraue Oberflaechen", "Verschleissbedingter Materialabtrag"},
- TypicalHarm: "Hautabschuerfungen, Schnittverletzungen an Haenden und Armen",
- RelevantLifecyclePhases: []string{"normal_operation", "maintenance", "setup"},
- RecommendedMeasuresDesign: []string{"Kanten brechen oder abrunden", "Glatte Oberflaechen an Kontaktstellen"},
- RecommendedMeasuresTechnical: []string{"Kantenschutzprofile anbringen"},
- RecommendedMeasuresInformation: []string{"Hinweis auf scharfe Kanten", "Handschuhpflicht in der Betriebsanweisung"},
- SuggestedEvidence: []string{"Oberflaechenpruefung", "Risikobeurteilung"},
- RelatedKeywords: []string{"Grat", "Scharfkantig", "Oberflaeche"},
- IsBuiltin: true,
- TenantID: nil,
- CreatedAt: now,
- },
- {
- ID: hazardUUID("mechanical_hazard", 18),
- Category: "mechanical_hazard",
- SubCategory: "hochdruckstrahl",
- Name: "Fluessigkeitshochdruckstrahl",
- Description: "Hochdruckstrahlen aus Hydraulik-, Kuehl- oder Reinigungssystemen koennen Haut durchdringen und schwere Gewebeschaeden verursachen.",
- DefaultSeverity: 5,
- DefaultProbability: 2,
- DefaultExposure: 2,
- DefaultAvoidance: 2,
- ApplicableComponentTypes: []string{"mechanical", "actuator"},
- RegulationReferences: []string{"Maschinenverordnung 2023/1230 Anhang I"},
- SuggestedMitigations: mustMarshalJSON([]string{"Abschirmung von Hochdruckleitungen", "Regelmaessige Leitungsinspektion"}),
- TypicalCauses: []string{"Leitungsbruch unter Hochdruck", "Undichte Verschraubungen", "Alterung von Schlauchleitungen"},
- TypicalHarm: "Hochdruckinjektionsverletzungen, Gewebsnekrose",
- RelevantLifecyclePhases: []string{"normal_operation", "maintenance"},
- RecommendedMeasuresDesign: []string{"Schlauchbruchsicherungen einbauen", "Leitungen ausserhalb des Aufenthaltsbereichs verlegen"},
- RecommendedMeasuresTechnical: []string{"Druckabschaltung bei Leitungsbruch", "Schutzblechverkleidung"},
- RecommendedMeasuresInformation: []string{"Warnhinweis an Hochdruckleitungen", "Prueffristen fuer Schlauchleitungen"},
- SuggestedEvidence: []string{"Druckpruefprotokoll", "Inspektionsbericht Schlauchleitungen"},
- RelatedKeywords: []string{"Hochdruck", "Hydraulikleitung", "Injection"},
- IsBuiltin: true,
- TenantID: nil,
- CreatedAt: now,
- },
- {
- ID: hazardUUID("mechanical_hazard", 19),
- Category: "mechanical_hazard",
- SubCategory: "federelemente",
- Name: "Gefahr durch federgespannte Elemente",
- Description: "Unter Spannung stehende Federn oder elastische Elemente koennen bei unkontrolliertem Loesen Teile wegschleudern oder Personen verletzen.",
- DefaultSeverity: 4,
- DefaultProbability: 2,
- DefaultExposure: 2,
- DefaultAvoidance: 3,
- ApplicableComponentTypes: []string{"mechanical"},
- RegulationReferences: []string{"Maschinenverordnung 2023/1230 Anhang I"},
- SuggestedMitigations: mustMarshalJSON([]string{"Gesicherte Federentspannung vor Demontage", "Warnung bei vorgespannten Elementen"}),
- TypicalCauses: []string{"Demontage ohne vorherige Entspannung", "Materialermuedung der Feder", "Fehlende Kennzeichnung vorgespannter Elemente"},
- TypicalHarm: "Verletzungen durch wegschleudernde Federelemente, Prellungen",
- RelevantLifecyclePhases: []string{"maintenance", "decommissioning"},
- RecommendedMeasuresDesign: []string{"Sichere Entspannungsmoeglichkeit vorsehen", "Federn mit Bruchsicherung"},
- RecommendedMeasuresTechnical: []string{"Spezialwerkzeug zur Federentspannung"},
- RecommendedMeasuresInformation: []string{"Kennzeichnung vorgespannter Elemente", "Wartungsanweisung mit Entspannungsprozedur"},
- SuggestedEvidence: []string{"Wartungsanweisung", "Risikobeurteilung"},
- RelatedKeywords: []string{"Feder", "Vorspannung", "Energiespeicher"},
- IsBuiltin: true,
- TenantID: nil,
- CreatedAt: now,
- },
- {
- ID: hazardUUID("mechanical_hazard", 20),
- Category: "mechanical_hazard",
- SubCategory: "schutztor",
- Name: "Quetschgefahr im Schliessbereich von Schutztoren",
- Description: "Automatisch schliessende Schutztore und -tueren koennen Personen im Schliessbereich einklemmen oder quetschen.",
- DefaultSeverity: 4,
- DefaultProbability: 3,
- DefaultExposure: 4,
- DefaultAvoidance: 3,
- ApplicableComponentTypes: []string{"mechanical", "actuator", "sensor"},
- RegulationReferences: []string{"Maschinenverordnung 2023/1230 Anhang I"},
- SuggestedMitigations: mustMarshalJSON([]string{"Schliesskantensicherung mit Kontaktleiste", "Lichtschranke im Schliessbereich"}),
- TypicalCauses: []string{"Fehlende Schliesskantensicherung", "Defekter Sensor", "Person im Schliessbereich nicht erkannt"},
- TypicalHarm: "Quetschverletzungen an Koerper oder Gliedmassen",
- RelevantLifecyclePhases: []string{"normal_operation", "maintenance"},
- RecommendedMeasuresDesign: []string{"Schliesskraftbegrenzung", "Reversierautomatik bei Hindernis"},
- RecommendedMeasuresTechnical: []string{"Kontaktleiste an der Schliesskante", "Lichtschranke im Durchgangsbereich"},
- RecommendedMeasuresInformation: []string{"Warnhinweis am Schutztor", "Automatik-Betrieb kennzeichnen"},
- SuggestedEvidence: []string{"Schliesskraftmessung", "Funktionstest Reversierautomatik"},
- RelatedKeywords: []string{"Schutztor", "Schliesskante", "Einklemmen"},
- IsBuiltin: true,
- TenantID: nil,
- CreatedAt: now,
- },
- // ====================================================================
- // Category: electrical_hazard (indices 7-10, 4 entries)
- // ====================================================================
- {
- ID: hazardUUID("electrical_hazard", 7),
- Category: "electrical_hazard",
- SubCategory: "lichtbogen",
- Name: "Lichtbogengefahr bei Schalthandlungen",
- Description: "Beim Schalten unter Last kann ein Lichtbogen entstehen, der zu Verbrennungen und Augenschaeden fuehrt.",
- DefaultSeverity: 5,
- DefaultProbability: 2,
- DefaultExposure: 2,
- DefaultAvoidance: 2,
- ApplicableComponentTypes: []string{"electrical"},
- RegulationReferences: []string{"Maschinenverordnung 2023/1230 Anhang I"},
- SuggestedMitigations: mustMarshalJSON([]string{"Lichtbogenschutzkleidung (PSA)", "Fernbediente Schaltgeraete"}),
- TypicalCauses: []string{"Schalten unter Last", "Verschmutzte Kontakte", "Fehlbedienung bei Wartung"},
- TypicalHarm: "Verbrennungen durch Lichtbogen, Augenschaeden, Druckwelle",
- RelevantLifecyclePhases: []string{"maintenance", "fault_finding"},
- RecommendedMeasuresDesign: []string{"Lasttrennschalter mit Lichtbogenkammer", "Beruerungssichere Klemmleisten"},
- RecommendedMeasuresTechnical: []string{"Lichtbogen-Erkennungssystem", "Fernausloesemoeglichkeit"},
- RecommendedMeasuresInformation: []string{"PSA-Pflicht bei Schalthandlungen", "Schaltbefugnisregelung"},
- SuggestedEvidence: []string{"Lichtbogenberechnung", "PSA-Ausstattungsnachweis"},
- RelatedKeywords: []string{"Lichtbogen", "Schalthandlung", "Arc Flash"},
- IsBuiltin: true,
- TenantID: nil,
- CreatedAt: now,
- },
- {
- ID: hazardUUID("electrical_hazard", 8),
- Category: "electrical_hazard",
- SubCategory: "ueberstrom",
- Name: "Ueberstrom durch Kurzschluss",
- Description: "Ein Kurzschluss kann zu extrem hohen Stroemen fuehren, die Leitungen ueberhitzen, Braende ausloesen und Bauteile zerstoeren.",
- DefaultSeverity: 4,
- DefaultProbability: 2,
- DefaultExposure: 2,
- DefaultAvoidance: 3,
- ApplicableComponentTypes: []string{"electrical"},
- RegulationReferences: []string{"Maschinenverordnung 2023/1230 Anhang I"},
- SuggestedMitigations: mustMarshalJSON([]string{"Selektive Absicherung mit Schmelzsicherungen", "Kurzschlussberechnung und Abschaltzeitnachweis"}),
- TypicalCauses: []string{"Beschaedigte Leitungsisolierung", "Feuchtigkeitseintritt", "Fehlerhafte Verdrahtung"},
- TypicalHarm: "Brandgefahr, Zerstoerung elektrischer Betriebsmittel",
- RelevantLifecyclePhases: []string{"normal_operation", "maintenance", "installation"},
- RecommendedMeasuresDesign: []string{"Kurzschlussfeste Dimensionierung der Leitungen", "Selektive Schutzkoordination"},
- RecommendedMeasuresTechnical: []string{"Leitungsschutzschalter", "Fehlerstrom-Schutzeinrichtung"},
- RecommendedMeasuresInformation: []string{"Stromlaufplan aktuell halten", "Prueffristen fuer elektrische Anlage"},
- SuggestedEvidence: []string{"Kurzschlussberechnung", "Pruefprotokoll nach DGUV V3"},
- RelatedKeywords: []string{"Kurzschluss", "Ueberstrom", "Leitungsschutz"},
- IsBuiltin: true,
- TenantID: nil,
- CreatedAt: now,
- },
- {
- ID: hazardUUID("electrical_hazard", 9),
- Category: "electrical_hazard",
- SubCategory: "erdungsfehler",
- Name: "Erdungsfehler im Schutzleitersystem",
- Description: "Ein unterbrochener oder fehlerhafter Schutzleiter verhindert die sichere Ableitung von Fehlerstroemen und macht Gehaeuse spannungsfuehrend.",
- DefaultSeverity: 5,
- DefaultProbability: 2,
- DefaultExposure: 3,
- DefaultAvoidance: 2,
- ApplicableComponentTypes: []string{"electrical"},
- RegulationReferences: []string{"Maschinenverordnung 2023/1230 Anhang I"},
- SuggestedMitigations: mustMarshalJSON([]string{"Regelmaessige Schutzleiterpruefung", "Fehlerstrom-Schutzschalter als Zusatzmassnahme"}),
- TypicalCauses: []string{"Lose Schutzleiterklemme", "Korrosion an Erdungspunkten", "Vergessener Schutzleiteranschluss nach Wartung"},
- TypicalHarm: "Elektrischer Schlag bei Beruehrung des Maschinengehaeuses",
- RelevantLifecyclePhases: []string{"normal_operation", "maintenance", "installation"},
- RecommendedMeasuresDesign: []string{"Redundante Schutzleiteranschluesse", "Schutzleiter-Monitoring"},
- RecommendedMeasuresTechnical: []string{"RCD-Schutzschalter 30 mA", "Isolationsueberwachung"},
- RecommendedMeasuresInformation: []string{"Pruefplaketten an Schutzleiterpunkten", "Prueffrist 12 Monate"},
- SuggestedEvidence: []string{"Schutzleitermessung", "Pruefprotokoll DGUV V3"},
- RelatedKeywords: []string{"Schutzleiter", "Erdung", "Fehlerstrom"},
- IsBuiltin: true,
- TenantID: nil,
- CreatedAt: now,
- },
- {
- ID: hazardUUID("electrical_hazard", 10),
- Category: "electrical_hazard",
- SubCategory: "isolationsversagen",
- Name: "Isolationsversagen in Hochspannungsbereich",
- Description: "Alterung, Verschmutzung oder mechanische Beschaedigung der Isolierung in Hochspannungsbereichen kann zu Spannungsueberschlaegen und Koerperdurchstroemung fuehren.",
- DefaultSeverity: 5,
- DefaultProbability: 2,
- DefaultExposure: 2,
- DefaultAvoidance: 2,
- ApplicableComponentTypes: []string{"electrical"},
- RegulationReferences: []string{"Maschinenverordnung 2023/1230 Anhang I"},
- SuggestedMitigations: mustMarshalJSON([]string{"Isolationswiderstandsmessung", "Spannungsfeste Einhausung"}),
- TypicalCauses: []string{"Alterung der Isolierstoffe", "Mechanische Beschaedigung", "Verschmutzung und Feuchtigkeit"},
- TypicalHarm: "Toedlicher Stromschlag, Verbrennungen durch Spannungsueberschlag",
- RelevantLifecyclePhases: []string{"normal_operation", "maintenance"},
- RecommendedMeasuresDesign: []string{"Verstaerkte Isolierung in kritischen Bereichen", "Luftstrecken und Kriechstrecken einhalten"},
- RecommendedMeasuresTechnical: []string{"Isolationsueberwachungsgeraet", "Verriegelter Zugang zum Hochspannungsbereich"},
- RecommendedMeasuresInformation: []string{"Hochspannungswarnung", "Zutrittsregelung fuer Elektrofachkraefte"},
- SuggestedEvidence: []string{"Isolationsmessprotokoll", "Pruefbericht Hochspannungsbereich"},
- RelatedKeywords: []string{"Isolation", "Hochspannung", "Durchschlag"},
- IsBuiltin: true,
- TenantID: nil,
- CreatedAt: now,
- },
- // ====================================================================
- // Category: thermal_hazard (indices 5-8, 4 entries)
- // ====================================================================
- {
- ID: hazardUUID("thermal_hazard", 5),
- Category: "thermal_hazard",
- SubCategory: "kaeltekontakt",
- Name: "Kontakt mit kalten Oberflaechen (Kryotechnik)",
- Description: "In kryotechnischen Anlagen oder Kuehlsystemen koennen extrem kalte Oberflaechen bei Beruehrung Kaelteverbrennungen verursachen.",
- DefaultSeverity: 4,
- DefaultProbability: 2,
- DefaultExposure: 2,
- DefaultAvoidance: 3,
- ApplicableComponentTypes: []string{"mechanical", "other"},
- RegulationReferences: []string{"Maschinenverordnung 2023/1230 Anhang I"},
- SuggestedMitigations: mustMarshalJSON([]string{"Isolierung kalter Oberflaechen", "Kaelteschutzhandschuhe"}),
- TypicalCauses: []string{"Fehlende Isolierung an Kryoleitungen", "Beruehrung tiefgekuehlter Bauteile", "Defekte Kaelteisolierung"},
- TypicalHarm: "Kaelteverbrennungen an Haenden und Fingern",
- RelevantLifecyclePhases: []string{"normal_operation", "maintenance"},
- RecommendedMeasuresDesign: []string{"Isolierung aller kalten Oberflaechen im Zugriffsbereich", "Abstandshalter zu Kryoleitungen"},
- RecommendedMeasuresTechnical: []string{"Temperaturwarnung bei kritischen Oberflaechentemperaturen"},
- RecommendedMeasuresInformation: []string{"Warnhinweis Kaeltegefahr", "PSA-Pflicht Kaelteschutz"},
- SuggestedEvidence: []string{"Oberflaechentemperaturmessung", "Risikobeurteilung"},
- RelatedKeywords: []string{"Kryotechnik", "Kaelte", "Kaelteverbrennung"},
- IsBuiltin: true,
- TenantID: nil,
- CreatedAt: now,
- },
- {
- ID: hazardUUID("thermal_hazard", 6),
- Category: "thermal_hazard",
- SubCategory: "waermestrahlung",
- Name: "Waermestrahlung von Hochtemperaturprozessen",
- Description: "Oefen, Giessereianlagen oder Waermebehandlungsprozesse emittieren intensive Waermestrahlung, die auch ohne direkten Kontakt zu Verbrennungen fuehren kann.",
- DefaultSeverity: 4,
- DefaultProbability: 3,
- DefaultExposure: 3,
- DefaultAvoidance: 3,
- ApplicableComponentTypes: []string{"mechanical", "other"},
- RegulationReferences: []string{"Maschinenverordnung 2023/1230 Anhang I"},
- SuggestedMitigations: mustMarshalJSON([]string{"Waermeschutzschilder", "Hitzeschutzkleidung"}),
- TypicalCauses: []string{"Offene Ofentuer bei Beschickung", "Fehlende Abschirmung", "Langzeitexposition in der Naehe von Waermequellen"},
- TypicalHarm: "Hautverbrennungen durch Waermestrahlung, Hitzschlag",
- RelevantLifecyclePhases: []string{"normal_operation", "setup"},
- RecommendedMeasuresDesign: []string{"Waermedaemmung und Strahlungsschilde", "Automatische Beschickung statt manueller"},
- RecommendedMeasuresTechnical: []string{"Waermestrahlung-Sensor mit Warnung", "Luftschleier vor Ofenoeeffnungen"},
- RecommendedMeasuresInformation: []string{"Maximalaufenthaltsdauer festlegen", "Hitzeschutz-PSA vorschreiben"},
- SuggestedEvidence: []string{"Waermestrahlungsmessung am Arbeitsplatz", "Risikobeurteilung"},
- RelatedKeywords: []string{"Waermestrahlung", "Ofen", "Hitzeschutz"},
- IsBuiltin: true,
- TenantID: nil,
- CreatedAt: now,
- },
- {
- ID: hazardUUID("thermal_hazard", 7),
- Category: "thermal_hazard",
- SubCategory: "brandgefahr",
- Name: "Brandgefahr durch ueberhitzte Antriebe",
- Description: "Ueberlastete oder schlecht gekuehlte Elektromotoren und Antriebe koennen sich so stark erhitzen, dass umgebende Materialien entzuendet werden.",
- DefaultSeverity: 5,
- DefaultProbability: 2,
- DefaultExposure: 3,
- DefaultAvoidance: 3,
- ApplicableComponentTypes: []string{"actuator", "electrical"},
- RegulationReferences: []string{"Maschinenverordnung 2023/1230 Anhang I"},
- SuggestedMitigations: mustMarshalJSON([]string{"Temperatursensor am Motor", "Thermischer Motorschutz"}),
- TypicalCauses: []string{"Dauerbetrieb ueber Nennlast", "Blockierter Kuehlluftstrom", "Defektes Motorlager erhoecht Reibung"},
- TypicalHarm: "Brand mit Sachschaeden und Personengefaehrdung durch Rauchentwicklung",
- RelevantLifecyclePhases: []string{"normal_operation"},
- RecommendedMeasuresDesign: []string{"Thermische Motorschutzdimensionierung", "Brandschottung um Antriebsbereich"},
- RecommendedMeasuresTechnical: []string{"PTC-Temperaturfuehler im Motor", "Rauchmelder im Antriebsbereich"},
- RecommendedMeasuresInformation: []string{"Wartungsintervalle fuer Kuehlluftwege", "Brandschutzordnung"},
- SuggestedEvidence: []string{"Temperaturmessung unter Last", "Brandschutzkonzept"},
- RelatedKeywords: []string{"Motorueberhitzung", "Brand", "Thermischer Schutz"},
- IsBuiltin: true,
- TenantID: nil,
- CreatedAt: now,
- },
- {
- ID: hazardUUID("thermal_hazard", 8),
- Category: "thermal_hazard",
- SubCategory: "heisse_fluessigkeiten",
- Name: "Verbrennungsgefahr durch heisse Fluessigkeiten",
- Description: "Heisse Prozessfluessigkeiten, Kuehlmittel oder Dampf koennen bei Leckage oder beim Oeffnen von Verschluessen Verbruehungen verursachen.",
- DefaultSeverity: 4,
- DefaultProbability: 3,
- DefaultExposure: 3,
- DefaultAvoidance: 3,
- ApplicableComponentTypes: []string{"mechanical", "other"},
- RegulationReferences: []string{"Maschinenverordnung 2023/1230 Anhang I"},
- SuggestedMitigations: mustMarshalJSON([]string{"Druckentlastung vor dem Oeffnen", "Spritzschutz an Leitungsverbindungen"}),
- TypicalCauses: []string{"Oeffnen von Verschluessen unter Druck", "Schlauchbruch bei heissem Medium", "Spritzer beim Nachfuellen"},
- TypicalHarm: "Verbruehungen an Haut und Augen",
- RelevantLifecyclePhases: []string{"normal_operation", "maintenance"},
- RecommendedMeasuresDesign: []string{"Druckentlastungsventil vor Verschluss", "Isolierte Leitungsfuehrung"},
- RecommendedMeasuresTechnical: []string{"Temperaturanzeige an kritischen Punkten", "Auffangwannen unter Leitungsverbindungen"},
- RecommendedMeasuresInformation: []string{"Warnhinweis heisse Fluessigkeit", "Abkuehlprozedur in Betriebsanweisung"},
- SuggestedEvidence: []string{"Temperaturmessung am Austritt", "Risikobeurteilung"},
- RelatedKeywords: []string{"Verbruehung", "Heisse Fluessigkeit", "Dampf"},
- IsBuiltin: true,
- TenantID: nil,
- CreatedAt: now,
- },
- // ====================================================================
- // Category: pneumatic_hydraulic (indices 1-10, 10 entries)
- // ====================================================================
- {
- ID: hazardUUID("pneumatic_hydraulic", 1),
- Category: "pneumatic_hydraulic",
- SubCategory: "druckverlust",
- Name: "Unkontrollierter Druckverlust in pneumatischem System",
- Description: "Ein ploetzlicher Druckabfall im Pneumatiksystem kann zum Versagen von Halte- und Klemmfunktionen fuehren, wodurch Werkstuecke herabfallen oder Achsen absacken.",
- DefaultSeverity: 4,
- DefaultProbability: 3,
- DefaultExposure: 3,
- DefaultAvoidance: 3,
- ApplicableComponentTypes: []string{"actuator", "mechanical"},
- RegulationReferences: []string{"Maschinenverordnung 2023/1230 Anhang I"},
- SuggestedMitigations: mustMarshalJSON([]string{"Rueckschlagventile in Haltezylinderleitungen", "Druckueberwachung mit sicherer Abschaltung"}),
- TypicalCauses: []string{"Kompressorausfall", "Leckage in der Versorgungsleitung", "Fehlerhaftes Druckregelventil"},
- TypicalHarm: "Quetschverletzungen durch absackende Achsen oder herabfallende Werkstuecke",
- RelevantLifecyclePhases: []string{"normal_operation", "fault_finding"},
- RecommendedMeasuresDesign: []string{"Mechanische Haltebremsen als Rueckfallebene", "Rueckschlagventile in sicherheitsrelevanten Leitungen"},
- RecommendedMeasuresTechnical: []string{"Druckwaechter mit sicherer Reaktion", "Druckspeicher fuer Notbetrieb"},
- RecommendedMeasuresInformation: []string{"Warnung bei Druckabfall", "Verfahrensanweisung fuer Druckausfall"},
- SuggestedEvidence: []string{"Druckabfalltest", "Risikobeurteilung"},
- RelatedKeywords: []string{"Druckverlust", "Pneumatik", "Haltefunktion"},
- IsBuiltin: true,
- TenantID: nil,
- CreatedAt: now,
- },
- {
- ID: hazardUUID("pneumatic_hydraulic", 2),
- Category: "pneumatic_hydraulic",
- SubCategory: "druckfreisetzung",
- Name: "Ploetzliche Druckfreisetzung bei Leitungsbruch",
- Description: "Ein Bersten oder Abreissen einer Druckleitung setzt schlagartig Energie frei, wobei Medien und Leitungsbruchstuecke weggeschleudert werden.",
- DefaultSeverity: 5,
- DefaultProbability: 2,
- DefaultExposure: 3,
- DefaultAvoidance: 2,
- ApplicableComponentTypes: []string{"mechanical", "actuator"},
- RegulationReferences: []string{"Maschinenverordnung 2023/1230 Anhang I"},
- SuggestedMitigations: mustMarshalJSON([]string{"Schlauchbruchsicherungen", "Druckfeste Leitungsverlegung"}),
- TypicalCauses: []string{"Materialermuedung der Leitung", "Ueberdruckbetrieb", "Mechanische Beschaedigung der Leitung"},
- TypicalHarm: "Verletzungen durch weggeschleuderte Leitungsteile und austretende Druckmedien",
- RelevantLifecyclePhases: []string{"normal_operation", "maintenance"},
- RecommendedMeasuresDesign: []string{"Berstdruckfest dimensionierte Leitungen", "Leitungen in Schutzrohren verlegen"},
- RecommendedMeasuresTechnical: []string{"Durchflussbegrenzer nach Druckquelle", "Schlauchbruchventile"},
- RecommendedMeasuresInformation: []string{"Prueffristen fuer Druckleitungen", "Warnhinweis an Hochdruckbereichen"},
- SuggestedEvidence: []string{"Druckpruefprotokoll", "Inspektionsbericht Leitungen"},
- RelatedKeywords: []string{"Leitungsbruch", "Druckfreisetzung", "Bersten"},
- IsBuiltin: true,
- TenantID: nil,
- CreatedAt: now,
- },
- {
- ID: hazardUUID("pneumatic_hydraulic", 3),
- Category: "pneumatic_hydraulic",
- SubCategory: "schlauchpeitschen",
- Name: "Schlauchpeitschen durch Berstversagen",
- Description: "Ein unter Druck stehender Schlauch kann bei Versagen unkontrolliert umherschlagen und Personen im Umfeld treffen.",
- DefaultSeverity: 4,
- DefaultProbability: 2,
- DefaultExposure: 3,
- DefaultAvoidance: 2,
- ApplicableComponentTypes: []string{"mechanical"},
- RegulationReferences: []string{"Maschinenverordnung 2023/1230 Anhang I"},
- SuggestedMitigations: mustMarshalJSON([]string{"Fangseile an Schlauchleitungen", "Schlauchbruchventile"}),
- TypicalCauses: []string{"Alterung des Schlauchmaterials", "Knicke in der Schlauchfuehrung", "Falsche Schlauchtype fuer das Medium"},
- TypicalHarm: "Peitschenverletzungen, Prellungen, Augenverletzungen",
- RelevantLifecyclePhases: []string{"normal_operation", "maintenance"},
- RecommendedMeasuresDesign: []string{"Fangseile oder Ketten an allen Schlauchleitungen", "Festverrohrung statt Schlauch wo moeglich"},
- RecommendedMeasuresTechnical: []string{"Schlauchbruchventil am Anschluss"},
- RecommendedMeasuresInformation: []string{"Tauschintervalle fuer Schlauchleitungen", "Kennzeichnung mit Herstelldatum"},
- SuggestedEvidence: []string{"Schlauchleitungspruefprotokoll", "Risikobeurteilung"},
- RelatedKeywords: []string{"Schlauch", "Peitschen", "Fangseil"},
- IsBuiltin: true,
- TenantID: nil,
- CreatedAt: now,
- },
- {
- ID: hazardUUID("pneumatic_hydraulic", 4),
- Category: "pneumatic_hydraulic",
- SubCategory: "druckspeicherenergie",
- Name: "Unerwartete Bewegung durch Druckspeicherrestenergie",
- Description: "Nach dem Abschalten der Maschine kann in Druckspeichern verbliebene Energie unerwartete Bewegungen von Zylindern oder Aktoren verursachen.",
- DefaultSeverity: 5,
- DefaultProbability: 3,
- DefaultExposure: 2,
- DefaultAvoidance: 2,
- ApplicableComponentTypes: []string{"actuator", "mechanical"},
- RegulationReferences: []string{"Maschinenverordnung 2023/1230 Anhang I"},
- SuggestedMitigations: mustMarshalJSON([]string{"Automatische Druckspeicher-Entladung bei Abschaltung", "Sperrventile vor Aktoren"}),
- TypicalCauses: []string{"Nicht entladener Druckspeicher", "Fehlendes Entlastungsventil", "Wartungszugriff ohne Druckfreischaltung"},
- TypicalHarm: "Quetsch- und Stossverletzungen durch unerwartete Zylinderbewegung",
- RelevantLifecyclePhases: []string{"maintenance", "fault_finding", "decommissioning"},
- RecommendedMeasuresDesign: []string{"Automatische Speicherentladung bei Hauptschalter-Aus", "Manuelles Entlastungsventil mit Druckanzeige"},
- RecommendedMeasuresTechnical: []string{"Druckmanometer am Speicher", "Verriegeltes Entlastungsventil"},
- RecommendedMeasuresInformation: []string{"Warnschild Druckspeicher", "LOTO-Verfahren fuer Druckspeicher"},
- SuggestedEvidence: []string{"Funktionstest Speicherentladung", "Risikobeurteilung"},
- RelatedKeywords: []string{"Druckspeicher", "Restenergie", "Speicherentladung"},
- IsBuiltin: true,
- TenantID: nil,
- CreatedAt: now,
- },
- {
- ID: hazardUUID("pneumatic_hydraulic", 5),
- Category: "pneumatic_hydraulic",
- SubCategory: "oelkontamination",
- Name: "Kontamination von Hydraulikoel durch Partikel",
- Description: "Verunreinigungen im Hydraulikoel fuehren zu erhoehtem Verschleiss an Ventilen und Dichtungen, was Leckagen und Funktionsversagen ausloest.",
- DefaultSeverity: 3,
- DefaultProbability: 3,
- DefaultExposure: 3,
- DefaultAvoidance: 4,
- ApplicableComponentTypes: []string{"actuator", "mechanical"},
- RegulationReferences: []string{"Maschinenverordnung 2023/1230 Anhang I"},
- SuggestedMitigations: mustMarshalJSON([]string{"Feinfilterung des Hydraulikoels", "Regelmaessige Oelanalyse"}),
- TypicalCauses: []string{"Verschleisspartikel im System", "Verschmutzte Nachfuellung", "Defekte Filterelemente"},
- TypicalHarm: "Maschinenausfall mit Folgeverletzungen durch ploetzliches Versagen hydraulischer Funktionen",
- RelevantLifecyclePhases: []string{"normal_operation", "maintenance"},
- RecommendedMeasuresDesign: []string{"Mehrfachfiltration mit Bypass-Anzeige", "Geschlossener Nachfuellkreislauf"},
- RecommendedMeasuresTechnical: []string{"Online-Partikelzaehler", "Differenzdruckanzeige am Filter"},
- RecommendedMeasuresInformation: []string{"Oelwechselintervalle festlegen", "Sauberkeitsvorgaben fuer Nachfuellung"},
- SuggestedEvidence: []string{"Oelanalysebericht", "Filterwechselprotokoll"},
- RelatedKeywords: []string{"Hydraulikoel", "Kontamination", "Filtration"},
- IsBuiltin: true,
- TenantID: nil,
- CreatedAt: now,
- },
- {
- ID: hazardUUID("pneumatic_hydraulic", 6),
- Category: "pneumatic_hydraulic",
- SubCategory: "leckage",
- Name: "Leckage an Hochdruckverbindungen",
- Description: "Undichte Verschraubungen oder Dichtungen an Hochdruckverbindungen fuehren zu Medienaustritt, Rutschgefahr und moeglichen Hochdruckinjektionsverletzungen.",
- DefaultSeverity: 4,
- DefaultProbability: 3,
- DefaultExposure: 3,
- DefaultAvoidance: 3,
- ApplicableComponentTypes: []string{"mechanical", "actuator"},
- RegulationReferences: []string{"Maschinenverordnung 2023/1230 Anhang I"},
- SuggestedMitigations: mustMarshalJSON([]string{"Leckagefreie Verschraubungen verwenden", "Auffangwannen unter Verbindungsstellen"}),
- TypicalCauses: []string{"Vibrationsbedingte Lockerung", "Alterung der Dichtungen", "Falsches Anzugsmoment"},
- TypicalHarm: "Rutschverletzungen, Hochdruckinjektion bei feinem Oelstrahl",
- RelevantLifecyclePhases: []string{"normal_operation", "maintenance"},
- RecommendedMeasuresDesign: []string{"Verschraubungen mit Sicherungsmitteln", "Leckage-Auffangvorrichtungen"},
- RecommendedMeasuresTechnical: []string{"Fuellstandsueberwachung im Tank", "Leckagesensor"},
- RecommendedMeasuresInformation: []string{"Sichtpruefung in Wartungsplan aufnehmen", "Hinweis auf Injektionsgefahr"},
- SuggestedEvidence: []string{"Leckagepruefprotokoll", "Risikobeurteilung"},
- RelatedKeywords: []string{"Leckage", "Verschraubung", "Hochdruck"},
- IsBuiltin: true,
- TenantID: nil,
- CreatedAt: now,
- },
- {
- ID: hazardUUID("pneumatic_hydraulic", 7),
- Category: "pneumatic_hydraulic",
- SubCategory: "kavitation",
- Name: "Kavitation in Hydraulikpumpe",
- Description: "Dampfblasenbildung und deren Implosion in der Hydraulikpumpe fuehren zu Materialabtrag, Leistungsverlust und ploetzlichem Pumpenversagen.",
- DefaultSeverity: 3,
- DefaultProbability: 2,
- DefaultExposure: 3,
- DefaultAvoidance: 4,
- ApplicableComponentTypes: []string{"actuator", "mechanical"},
- RegulationReferences: []string{"Maschinenverordnung 2023/1230 Anhang I"},
- SuggestedMitigations: mustMarshalJSON([]string{"Korrekte Saughoehe einhalten", "Saugleitungsdimensionierung pruefen"}),
- TypicalCauses: []string{"Zu kleine Saugleitung", "Verstopfter Saugfilter", "Zu hohe Oelviskositaet bei Kaelte"},
- TypicalHarm: "Maschinenausfall durch Pumpenversagen mit moeglichen Folgeverletzungen",
- RelevantLifecyclePhases: []string{"normal_operation", "setup"},
- RecommendedMeasuresDesign: []string{"Saugleitung grosszuegig dimensionieren", "Ueberdruck-Zulaufsystem"},
- RecommendedMeasuresTechnical: []string{"Vakuumanzeige an der Saugseite", "Temperaturueberwachung des Oels"},
- RecommendedMeasuresInformation: []string{"Vorwaermverfahren bei Kaeltestart", "Wartungsintervall Saugfilter"},
- SuggestedEvidence: []string{"Saugdruckmessung", "Pumpeninspektionsbericht"},
- RelatedKeywords: []string{"Kavitation", "Hydraulikpumpe", "Saugleitung"},
- IsBuiltin: true,
- TenantID: nil,
- CreatedAt: now,
- },
- {
- ID: hazardUUID("pneumatic_hydraulic", 8),
- Category: "pneumatic_hydraulic",
- SubCategory: "ueberdruckversagen",
- Name: "Ueberdruckversagen durch defektes Druckbegrenzungsventil",
- Description: "Ein klemmendes oder falsch eingestelltes Druckbegrenzungsventil laesst den Systemdruck unkontrolliert ansteigen, was zum Bersten von Komponenten fuehren kann.",
- DefaultSeverity: 5,
- DefaultProbability: 2,
- DefaultExposure: 3,
- DefaultAvoidance: 2,
- ApplicableComponentTypes: []string{"actuator", "mechanical"},
- RegulationReferences: []string{"Maschinenverordnung 2023/1230 Anhang I"},
- SuggestedMitigations: mustMarshalJSON([]string{"Redundantes Druckbegrenzungsventil", "Druckschalter mit Abschaltung"}),
- TypicalCauses: []string{"Verschmutztes Druckbegrenzungsventil", "Falsche Einstellung nach Wartung", "Ermuedung der Ventilfeder"},
- TypicalHarm: "Bersten von Leitungen und Gehaeusen mit Splitterwurf, Hochdruckinjektionsverletzungen",
- RelevantLifecyclePhases: []string{"normal_operation", "maintenance"},
- RecommendedMeasuresDesign: []string{"Redundante Druckbegrenzung", "Berstscheibe als letzte Sicherung"},
- RecommendedMeasuresTechnical: []string{"Druckschalter mit sicherer Pumpenabschaltung", "Manometer mit Schleppzeiger"},
- RecommendedMeasuresInformation: []string{"Pruefintervall Druckbegrenzungsventil", "Einstellprotokoll nach Wartung"},
- SuggestedEvidence: []string{"Ventilpruefprotokoll", "Druckverlaufsmessung"},
- RelatedKeywords: []string{"Ueberdruck", "Druckbegrenzungsventil", "Bersten"},
- IsBuiltin: true,
- TenantID: nil,
- CreatedAt: now,
- },
- {
- ID: hazardUUID("pneumatic_hydraulic", 9),
- Category: "pneumatic_hydraulic",
- SubCategory: "ventilversagen",
- Name: "Unkontrollierte Zylinderbewegung bei Ventilversagen",
- Description: "Bei Ausfall oder Fehlfunktion eines Wegeventils kann ein Zylinder unkontrolliert ein- oder ausfahren und Personen im Bewegungsbereich verletzen.",
- DefaultSeverity: 5,
- DefaultProbability: 2,
- DefaultExposure: 3,
- DefaultAvoidance: 2,
- ApplicableComponentTypes: []string{"actuator", "controller"},
- RegulationReferences: []string{"Maschinenverordnung 2023/1230 Anhang I"},
- SuggestedMitigations: mustMarshalJSON([]string{"Redundante Ventile fuer sicherheitskritische Achsen", "Lasthalteventile an Vertikalachsen"}),
- TypicalCauses: []string{"Elektromagnetausfall am Ventil", "Ventilschieber klemmt", "Kontamination blockiert Ventilsitz"},
- TypicalHarm: "Quetsch- und Stossverletzungen durch unkontrollierte Zylinderbewegung",
- RelevantLifecyclePhases: []string{"normal_operation", "fault_finding"},
- RecommendedMeasuresDesign: []string{"Redundante Ventilanordnung mit Ueberwachung", "Lasthalteventile fuer schwerkraftbelastete Achsen"},
- RecommendedMeasuresTechnical: []string{"Positionsueberwachung am Zylinder", "Ventil-Stellungsueberwachung"},
- RecommendedMeasuresInformation: []string{"Fehlermeldung bei Ventildiskrepanz", "Notfallprozedur bei Ventilversagen"},
- SuggestedEvidence: []string{"Funktionstest Redundanz", "FMEA Ventilschaltung"},
- RelatedKeywords: []string{"Wegeventil", "Zylinderversagen", "Ventilausfall"},
- IsBuiltin: true,
- TenantID: nil,
- CreatedAt: now,
- },
- {
- ID: hazardUUID("pneumatic_hydraulic", 10),
- Category: "pneumatic_hydraulic",
- SubCategory: "viskositaet",
- Name: "Temperaturbedingte Viskositaetsaenderung von Hydraulikmedium",
- Description: "Extreme Temperaturen veraendern die Viskositaet des Hydraulikoels so stark, dass Ventile und Pumpen nicht mehr zuverlaessig arbeiten und Sicherheitsfunktionen versagen.",
- DefaultSeverity: 3,
- DefaultProbability: 2,
- DefaultExposure: 2,
- DefaultAvoidance: 4,
- ApplicableComponentTypes: []string{"actuator", "mechanical"},
- RegulationReferences: []string{"Maschinenverordnung 2023/1230 Anhang I"},
- SuggestedMitigations: mustMarshalJSON([]string{"Oeltemperierung", "Oelsorte mit breitem Viskositaetsbereich"}),
- TypicalCauses: []string{"Kaltstart ohne Vorwaermung", "Ueberhitzung durch mangelnde Kuehlung", "Falsche Oelsorte"},
- TypicalHarm: "Funktionsversagen hydraulischer Sicherheitseinrichtungen",
- RelevantLifecyclePhases: []string{"normal_operation", "setup"},
- RecommendedMeasuresDesign: []string{"Oelkuehler und Oelheizung vorsehen", "Temperaturbereich der Oelsorte abstimmen"},
- RecommendedMeasuresTechnical: []string{"Oeltemperatursensor mit Warnmeldung", "Aufwaermprogramm in der Steuerung"},
- RecommendedMeasuresInformation: []string{"Zulaessiger Temperaturbereich in Betriebsanleitung", "Oelwechselvorschrift"},
- SuggestedEvidence: []string{"Temperaturverlaufsmessung", "Oeldatenblatt"},
- RelatedKeywords: []string{"Viskositaet", "Oeltemperatur", "Hydraulikmedium"},
- IsBuiltin: true,
- TenantID: nil,
- CreatedAt: now,
- },
- // ====================================================================
- // Category: noise_vibration (indices 1-6, 6 entries)
- // ====================================================================
- {
- ID: hazardUUID("noise_vibration", 1),
- Category: "noise_vibration",
- SubCategory: "dauerschall",
- Name: "Gehoerschaedigung durch Dauerschallpegel",
- Description: "Dauerhaft erhoehte Schallpegel am Arbeitsplatz ueber dem Grenzwert fuehren zu irreversiblen Gehoerschaeden bei den Maschinenbedienern.",
- DefaultSeverity: 4,
- DefaultProbability: 4,
- DefaultExposure: 4,
- DefaultAvoidance: 3,
- ApplicableComponentTypes: []string{"mechanical", "actuator"},
- RegulationReferences: []string{"Maschinenverordnung 2023/1230 Anhang I"},
- SuggestedMitigations: mustMarshalJSON([]string{"Laermminderung an der Quelle", "Gehoerschutzpflicht ab 85 dB(A)"}),
- TypicalCauses: []string{"Nicht gekapselte Antriebe", "Metallische Schlagvorgaenge", "Fehlende Schalldaemmung"},
- TypicalHarm: "Laermschwerhoerigkeit, Tinnitus",
- RelevantLifecyclePhases: []string{"normal_operation"},
- RecommendedMeasuresDesign: []string{"Laermarme Antriebe und Getriebe", "Schwingungsdaempfende Lagerung"},
- RecommendedMeasuresTechnical: []string{"Schallschutzkapseln", "Schallschutzwaende"},
- RecommendedMeasuresInformation: []string{"Laermbereichskennzeichnung", "Gehoerschutzpflicht beschildern"},
- SuggestedEvidence: []string{"Laermpegelmessung am Arbeitsplatz", "Laermkataster"},
- RelatedKeywords: []string{"Laerm", "Gehoerschutz", "Schallpegel"},
- IsBuiltin: true,
- TenantID: nil,
- CreatedAt: now,
- },
- {
- ID: hazardUUID("noise_vibration", 2),
- Category: "noise_vibration",
- SubCategory: "hand_arm_vibration",
- Name: "Hand-Arm-Vibrationssyndrom durch vibrierende Werkzeuge",
- Description: "Langzeitige Nutzung handgefuehrter vibrierender Werkzeuge kann zu Durchblutungsstoerungen, Nervenschaeden und Gelenkbeschwerden in Haenden und Armen fuehren.",
- DefaultSeverity: 4,
- DefaultProbability: 3,
- DefaultExposure: 4,
- DefaultAvoidance: 3,
- ApplicableComponentTypes: []string{"mechanical", "other"},
- RegulationReferences: []string{"Maschinenverordnung 2023/1230 Anhang I"},
- SuggestedMitigations: mustMarshalJSON([]string{"Vibrationsgedaempfte Werkzeuge verwenden", "Expositionszeit begrenzen"}),
- TypicalCauses: []string{"Ungepufferte Handgriffe", "Verschlissene Werkzeuge mit erhoehter Vibration", "Fehlende Arbeitszeitbegrenzung"},
- TypicalHarm: "Weissfingerkrankheit, Karpaltunnelsyndrom, Gelenkarthrose",
- RelevantLifecyclePhases: []string{"normal_operation", "maintenance"},
- RecommendedMeasuresDesign: []string{"Vibrationsgedaempfte Griffe", "Automatisierung statt Handarbeit"},
- RecommendedMeasuresTechnical: []string{"Vibrationsmessung am Werkzeug", "Anti-Vibrationshandschuhe"},
- RecommendedMeasuresInformation: []string{"Expositionsdauer dokumentieren", "Arbeitsmedizinische Vorsorge anbieten"},
- SuggestedEvidence: []string{"Vibrationsmessung nach ISO 5349", "Expositionsberechnung"},
- RelatedKeywords: []string{"Vibration", "Hand-Arm", "HAVS"},
- IsBuiltin: true,
- TenantID: nil,
- CreatedAt: now,
- },
- {
- ID: hazardUUID("noise_vibration", 3),
- Category: "noise_vibration",
- SubCategory: "ganzkoerpervibration",
- Name: "Ganzkoerpervibration an Bedienplaetzen",
- Description: "Vibrationen, die ueber den Sitz oder die Standflaeche auf den gesamten Koerper uebertragen werden, koennen zu Wirbelsaeulenschaeden fuehren.",
- DefaultSeverity: 3,
- DefaultProbability: 3,
- DefaultExposure: 4,
- DefaultAvoidance: 3,
- ApplicableComponentTypes: []string{"mechanical", "other"},
- RegulationReferences: []string{"Maschinenverordnung 2023/1230 Anhang I"},
- SuggestedMitigations: mustMarshalJSON([]string{"Schwingungsisolierter Fahrersitz", "Vibrationsgedaempfte Stehplattform"}),
- TypicalCauses: []string{"Unwucht in rotierenden Teilen", "Unebener Fahrweg", "Fehlende Schwingungsisolierung des Bedienplatzes"},
- TypicalHarm: "Bandscheibenschaeden, Rueckenschmerzen, Ermuedung",
- RelevantLifecyclePhases: []string{"normal_operation"},
- RecommendedMeasuresDesign: []string{"Schwingungsisolierte Kabine oder Plattform", "Auswuchten rotierender Massen"},
- RecommendedMeasuresTechnical: []string{"Luftgefederter Sitz", "Vibrationsueberwachung mit Grenzwertwarnung"},
- RecommendedMeasuresInformation: []string{"Maximalexpositionsdauer festlegen", "Arbeitsmedizinische Vorsorge"},
- SuggestedEvidence: []string{"Ganzkoerper-Vibrationsmessung nach ISO 2631", "Expositionsbewertung"},
- RelatedKeywords: []string{"Ganzkoerpervibration", "Wirbelsaeule", "Sitzvibrationen"},
- IsBuiltin: true,
- TenantID: nil,
- CreatedAt: now,
- },
- {
- ID: hazardUUID("noise_vibration", 4),
- Category: "noise_vibration",
- SubCategory: "impulslaerm",
- Name: "Impulslaerm durch Stanz-/Praegevorgaenge",
- Description: "Kurzzeitige Schallspitzen bei Stanz-, Praege- oder Nietvorgaengen ueberschreiten den Spitzenschalldruckpegel und schaedigen das Gehoer besonders stark.",
- DefaultSeverity: 4,
- DefaultProbability: 4,
- DefaultExposure: 3,
- DefaultAvoidance: 3,
- ApplicableComponentTypes: []string{"mechanical"},
- RegulationReferences: []string{"Maschinenverordnung 2023/1230 Anhang I"},
- SuggestedMitigations: mustMarshalJSON([]string{"Schalldaemmende Werkzeugeinhausung", "Impulsschallgedaempfter Gehoerschutz"}),
- TypicalCauses: []string{"Metall-auf-Metall-Schlag", "Offene Stanzwerkzeuge", "Fehlende Schalldaemmung"},
- TypicalHarm: "Akutes Knalltrauma, irreversible Gehoerschaedigung",
- RelevantLifecyclePhases: []string{"normal_operation"},
- RecommendedMeasuresDesign: []string{"Elastische Werkzeugauflagen", "Geschlossene Werkzeugkammer"},
- RecommendedMeasuresTechnical: []string{"Schallschutzkabine um Stanzbereich", "Impulslaermueberwachung"},
- RecommendedMeasuresInformation: []string{"Gehoerschutzpflicht-Kennzeichnung", "Schulung zur Impulslaermgefahr"},
- SuggestedEvidence: []string{"Spitzenpegelmessung", "Laermgutachten"},
- RelatedKeywords: []string{"Impulslaerm", "Stanzen", "Spitzenschallpegel"},
- IsBuiltin: true,
- TenantID: nil,
- CreatedAt: now,
- },
- {
- ID: hazardUUID("noise_vibration", 5),
- Category: "noise_vibration",
- SubCategory: "infraschall",
- Name: "Infraschall von Grossventilatoren",
- Description: "Grosse Ventilatoren und Geblaese erzeugen niederfrequenten Infraschall, der zu Unwohlsein, Konzentrationsstoerungen und Ermuedung fuehren kann.",
- DefaultSeverity: 3,
- DefaultProbability: 2,
- DefaultExposure: 3,
- DefaultAvoidance: 3,
- ApplicableComponentTypes: []string{"mechanical", "actuator"},
- RegulationReferences: []string{"Maschinenverordnung 2023/1230 Anhang I"},
- SuggestedMitigations: mustMarshalJSON([]string{"Schwingungsisolierte Aufstellung", "Schalldaempfer in Kanaelen"}),
- TypicalCauses: []string{"Grosse Ventilatorschaufeln mit niedriger Drehzahl", "Resonanzen in Luftkanaelen", "Fehlende Schwingungsentkopplung"},
- TypicalHarm: "Unwohlsein, Uebelkeit, Konzentrationsstoerungen bei Dauerexposition",
- RelevantLifecyclePhases: []string{"normal_operation"},
- RecommendedMeasuresDesign: []string{"Schwingungsentkopplung des Ventilators", "Resonanzfreie Kanaldimensionierung"},
- RecommendedMeasuresTechnical: []string{"Niederfrequenz-Schalldaempfer", "Infraschall-Messgeraet"},
- RecommendedMeasuresInformation: []string{"Aufklaerung ueber Infraschallsymptome", "Expositionshinweise"},
- SuggestedEvidence: []string{"Infraschallmessung", "Risikobeurteilung"},
- RelatedKeywords: []string{"Infraschall", "Ventilator", "Niederfrequenz"},
- IsBuiltin: true,
- TenantID: nil,
- CreatedAt: now,
- },
- {
- ID: hazardUUID("noise_vibration", 6),
- Category: "noise_vibration",
- SubCategory: "resonanz",
- Name: "Resonanzschwingungen in Maschinengestell",
- Description: "Anregung des Maschinengestells in seiner Eigenfrequenz kann zu unkontrollierten Schwingungen fuehren, die Bauteile ermueden und zum Versagen bringen.",
- DefaultSeverity: 4,
- DefaultProbability: 2,
- DefaultExposure: 3,
- DefaultAvoidance: 3,
- ApplicableComponentTypes: []string{"mechanical"},
- RegulationReferences: []string{"Maschinenverordnung 2023/1230 Anhang I"},
- SuggestedMitigations: mustMarshalJSON([]string{"Eigenfrequenzanalyse bei Konstruktion", "Schwingungsdaempfer anbringen"}),
- TypicalCauses: []string{"Drehzahl nahe der Eigenfrequenz des Gestells", "Fehlende Daempfungselemente", "Nachtraegliche Massenveraenderungen"},
- TypicalHarm: "Materialermuedungsbruch mit Absturz von Bauteilen, Verletzungen durch Bruchstuecke",
- RelevantLifecyclePhases: []string{"normal_operation", "setup"},
- RecommendedMeasuresDesign: []string{"Eigenfrequenz ausserhalb des Betriebsdrehzahlbereichs legen", "Versteifung des Gestells"},
- RecommendedMeasuresTechnical: []string{"Schwingungssensoren mit Grenzwertueberwachung", "Tilger oder Daempfer anbringen"},
- RecommendedMeasuresInformation: []string{"Verbotene Drehzahlbereiche kennzeichnen", "Schwingungsueberwachungsanleitung"},
- SuggestedEvidence: []string{"Modalanalyse des Gestells", "Schwingungsmessprotokoll"},
- RelatedKeywords: []string{"Resonanz", "Eigenfrequenz", "Strukturschwingung"},
- IsBuiltin: true,
- TenantID: nil,
- CreatedAt: now,
- },
- // ====================================================================
- // Category: ergonomic (indices 1-8, 8 entries)
- // ====================================================================
- {
- ID: hazardUUID("ergonomic", 1),
- Category: "ergonomic",
- SubCategory: "fehlbedienung",
- Name: "Fehlbedienung durch unguenstige Anordnung von Bedienelementen",
- Description: "Ungluecklich platzierte oder schlecht beschriftete Bedienelemente erhoehen das Risiko von Fehlbedienungen, die sicherheitskritische Maschinenbewegungen ausloesen.",
- DefaultSeverity: 4,
- DefaultProbability: 3,
- DefaultExposure: 4,
- DefaultAvoidance: 3,
- ApplicableComponentTypes: []string{"controller", "sensor"},
- RegulationReferences: []string{"Maschinenverordnung 2023/1230 Anhang I"},
- SuggestedMitigations: mustMarshalJSON([]string{"Ergonomische Anordnung nach ISO 9355", "Eindeutige Beschriftung und Farbcodierung"}),
- TypicalCauses: []string{"Nicht-intuitive Anordnung der Schalter", "Fehlende oder unlesbare Beschriftung", "Zu geringer Abstand zwischen Bedienelementen"},
- TypicalHarm: "Verletzungen durch unbeabsichtigte Maschinenaktionen nach Fehlbedienung",
- RelevantLifecyclePhases: []string{"normal_operation", "setup"},
- RecommendedMeasuresDesign: []string{"Bedienelemente nach ISO 9355 anordnen", "Farbcodierung und Symbolik nach IEC 60073"},
- RecommendedMeasuresTechnical: []string{"Bestaetigung fuer kritische Aktionen", "Abgedeckte Schalter fuer Gefahrenfunktionen"},
- RecommendedMeasuresInformation: []string{"Bedienerhandbuch mit Bilddarstellungen", "Schulung der Bediener"},
- SuggestedEvidence: []string{"Usability-Test des Bedienfeldes", "Risikobeurteilung"},
- RelatedKeywords: []string{"Bedienelemente", "Fehlbedienung", "Ergonomie"},
- IsBuiltin: true,
- TenantID: nil,
- CreatedAt: now,
- },
- {
- ID: hazardUUID("ergonomic", 2),
- Category: "ergonomic",
- SubCategory: "zwangshaltung",
- Name: "Zwangshaltung bei Beschickungsvorgaengen",
- Description: "Unglueckliche Koerperhaltungen beim manuellen Beladen oder Entnehmen von Werkstuecken fuehren zu muskuloskeletalen Beschwerden.",
- DefaultSeverity: 3,
- DefaultProbability: 4,
- DefaultExposure: 4,
- DefaultAvoidance: 3,
- ApplicableComponentTypes: []string{"mechanical", "other"},
- RegulationReferences: []string{"Maschinenverordnung 2023/1230 Anhang I"},
- SuggestedMitigations: mustMarshalJSON([]string{"Hoehenverstellbare Beschickungsoeffnung", "Automatische Materialzufuhr"}),
- TypicalCauses: []string{"Beschickungsoeffnung in unguenstiger Hoehe", "Grosse Greifentfernung", "Wiederholte Drehbewegungen des Rumpfes"},
- TypicalHarm: "Rueckenbeschwerden, Schulter-Arm-Syndrom, chronische Gelenkschmerzen",
- RelevantLifecyclePhases: []string{"normal_operation"},
- RecommendedMeasuresDesign: []string{"Beschickungshoehe zwischen 60 und 120 cm", "Kurze Greifwege"},
- RecommendedMeasuresTechnical: []string{"Hoehenverstellbare Arbeitstische", "Hebehilfen und Manipulatoren"},
- RecommendedMeasuresInformation: []string{"Ergonomie-Schulung", "Hinweise zur richtigen Koerperhaltung"},
- SuggestedEvidence: []string{"Ergonomische Arbeitsplatzanalyse", "Gefaehrdungsbeurteilung"},
- RelatedKeywords: []string{"Zwangshaltung", "Beschickung", "Muskel-Skelett"},
- IsBuiltin: true,
- TenantID: nil,
- CreatedAt: now,
- },
- {
- ID: hazardUUID("ergonomic", 3),
- Category: "ergonomic",
- SubCategory: "manuelle_handhabung",
- Name: "Koerperliche Ueberforderung durch manuelle Handhabung",
- Description: "Schwere Lasten muessen manuell gehoben, getragen oder verschoben werden, was zu akuten Verletzungen oder chronischen Schaeden fuehrt.",
- DefaultSeverity: 3,
- DefaultProbability: 4,
- DefaultExposure: 4,
- DefaultAvoidance: 3,
- ApplicableComponentTypes: []string{"mechanical", "other"},
- RegulationReferences: []string{"Maschinenverordnung 2023/1230 Anhang I"},
- SuggestedMitigations: mustMarshalJSON([]string{"Technische Hebehilfen bereitstellen", "Gewichtsgrenze fuer manuelles Heben festlegen"}),
- TypicalCauses: []string{"Fehlende Hebehilfen", "Zu schwere Einzelteile", "Haeufiges Heben ueber Schulterhoehe"},
- TypicalHarm: "Bandscheibenvorfall, Rueckenverletzungen, Ueberanstrengung",
- RelevantLifecyclePhases: []string{"normal_operation", "maintenance", "setup"},
- RecommendedMeasuresDesign: []string{"Bauteile unter 15 kg fuer manuelles Handling", "Hebevorrichtungen integrieren"},
- RecommendedMeasuresTechnical: []string{"Kran oder Hebezeug am Arbeitsplatz", "Vakuumheber fuer Platten"},
- RecommendedMeasuresInformation: []string{"Hebebelastungstabelle aushangen", "Unterweisung in Hebetechnik"},
- SuggestedEvidence: []string{"Lastenhandhabungsbeurteilung", "Risikobeurteilung"},
- RelatedKeywords: []string{"Heben", "Lastenhandhabung", "Ueberanstrengung"},
- IsBuiltin: true,
- TenantID: nil,
- CreatedAt: now,
- },
- {
- ID: hazardUUID("ergonomic", 4),
- Category: "ergonomic",
- SubCategory: "verwechslung",
- Name: "Verwechslungsgefahr bei gleichartigen Bedienelementen",
- Description: "Baugleiche, nicht unterscheidbare Taster oder Schalter koennen verwechselt werden, was zu unbeabsichtigten und gefaehrlichen Maschinenaktionen fuehrt.",
- DefaultSeverity: 4,
- DefaultProbability: 3,
- DefaultExposure: 4,
- DefaultAvoidance: 4,
- ApplicableComponentTypes: []string{"controller", "sensor"},
- RegulationReferences: []string{"Maschinenverordnung 2023/1230 Anhang I"},
- SuggestedMitigations: mustMarshalJSON([]string{"Unterschiedliche Formen und Farben fuer verschiedene Funktionen", "Beschriftung in Klartext"}),
- TypicalCauses: []string{"Identische Tasterform fuer unterschiedliche Funktionen", "Fehlende Beschriftung", "Schlechte Beleuchtung am Bedienfeld"},
- TypicalHarm: "Unbeabsichtigte Maschinenaktionen mit Verletzungsgefahr",
- RelevantLifecyclePhases: []string{"normal_operation", "setup"},
- RecommendedMeasuresDesign: []string{"Form- und Farbcodierung nach IEC 60073", "Raeumliche Gruppierung nach Funktionsbereichen"},
- RecommendedMeasuresTechnical: []string{"Beleuchtetes Bedienfeld", "Haptisch unterscheidbare Taster"},
- RecommendedMeasuresInformation: []string{"Bedienfeldplan am Arbeitsplatz aushangen", "Einweisung neuer Bediener"},
- SuggestedEvidence: []string{"Usability-Bewertung", "Risikobeurteilung"},
- RelatedKeywords: []string{"Verwechslung", "Taster", "Bedienpanel"},
- IsBuiltin: true,
- TenantID: nil,
- CreatedAt: now,
- },
- {
- ID: hazardUUID("ergonomic", 5),
- Category: "ergonomic",
- SubCategory: "sichtbehinderung",
- Name: "Sichtbehinderung des Gefahrenbereichs vom Bedienplatz",
- Description: "Vom Bedienplatz aus ist der Gefahrenbereich nicht vollstaendig einsehbar, sodass der Bediener Personen im Gefahrenbereich nicht erkennen kann.",
- DefaultSeverity: 5,
- DefaultProbability: 3,
- DefaultExposure: 4,
- DefaultAvoidance: 2,
- ApplicableComponentTypes: []string{"controller", "sensor", "mechanical"},
- RegulationReferences: []string{"Maschinenverordnung 2023/1230 Anhang I"},
- SuggestedMitigations: mustMarshalJSON([]string{"Kameraueberwachung des Gefahrenbereichs", "Sicherheits-Laserscanner"}),
- TypicalCauses: []string{"Verdeckte Sicht durch Maschinenteile", "Bedienplatz zu weit vom Arbeitsbereich", "Fehlende Spiegel oder Kameras"},
- TypicalHarm: "Schwere Verletzungen von Personen im nicht einsehbaren Gefahrenbereich",
- RelevantLifecyclePhases: []string{"normal_operation", "setup"},
- RecommendedMeasuresDesign: []string{"Bedienplatz mit direkter Sicht auf Gefahrenbereich", "Transparente Schutzeinrichtungen"},
- RecommendedMeasuresTechnical: []string{"Kamerasystem mit Monitor am Bedienplatz", "Sicherheits-Laserscanner"},
- RecommendedMeasuresInformation: []string{"Hinweis auf toten Winkel", "Anlaufwarnung vor Maschinenbewegung"},
- SuggestedEvidence: []string{"Sichtfeldanalyse vom Bedienplatz", "Risikobeurteilung"},
- RelatedKeywords: []string{"Sichtfeld", "Toter Winkel", "Bedienplatz"},
- IsBuiltin: true,
- TenantID: nil,
- CreatedAt: now,
- },
- {
- ID: hazardUUID("ergonomic", 6),
- Category: "ergonomic",
- SubCategory: "griffgestaltung",
- Name: "Unergonomische Griffgestaltung von Handwerkzeugen",
- Description: "Schlecht geformte Griffe an handgefuehrten Werkzeugen und Hebeln fuehren zu Ermuedung, verminderter Griffkraft und erhoehtem Unfallrisiko.",
- DefaultSeverity: 3,
- DefaultProbability: 3,
- DefaultExposure: 4,
- DefaultAvoidance: 4,
- ApplicableComponentTypes: []string{"mechanical", "other"},
- RegulationReferences: []string{"Maschinenverordnung 2023/1230 Anhang I"},
- SuggestedMitigations: mustMarshalJSON([]string{"Ergonomische Griffformen nach EN 894", "Rutschfeste Griffoberflaechen"}),
- TypicalCauses: []string{"Zu duenner oder zu dicker Griff", "Glatte Griffoberflaeche", "Scharfe Kanten am Griff"},
- TypicalHarm: "Sehnenscheidenentzuendung, Abrutschen mit Folgeverletzung",
- RelevantLifecyclePhases: []string{"normal_operation", "maintenance"},
- RecommendedMeasuresDesign: []string{"Griffdurchmesser 30-45 mm", "Anatomisch geformte Griffe"},
- RecommendedMeasuresTechnical: []string{"Rutschfeste Beschichtung", "Handgelenkschlaufe an Handwerkzeugen"},
- RecommendedMeasuresInformation: []string{"Auswahl ergonomischer Werkzeuge dokumentieren", "Arbeitsmedizinische Beratung"},
- SuggestedEvidence: []string{"Ergonomische Werkzeugbewertung", "Risikobeurteilung"},
- RelatedKeywords: []string{"Griff", "Handwerkzeug", "Ergonomie"},
- IsBuiltin: true,
- TenantID: nil,
- CreatedAt: now,
- },
- {
- ID: hazardUUID("ergonomic", 7),
- Category: "ergonomic",
- SubCategory: "monotonie",
- Name: "Monotone Taetigkeit fuehrt zu Aufmerksamkeitsverlust",
- Description: "Langandauernde, sich wiederholende Taetigkeiten ohne Abwechslung vermindern die Aufmerksamkeit des Bedieners und erhoehen die Unfallgefahr.",
- DefaultSeverity: 4,
- DefaultProbability: 3,
- DefaultExposure: 4,
- DefaultAvoidance: 3,
- ApplicableComponentTypes: []string{"other"},
- RegulationReferences: []string{"Maschinenverordnung 2023/1230 Anhang I"},
- SuggestedMitigations: mustMarshalJSON([]string{"Aufgabenwechsel organisieren", "Regelmaessige Pausen einplanen"}),
- TypicalCauses: []string{"Gleichfoermige Wiederholtaetigkeit", "Fehlende Pausenregelung", "Mangelnde Aufgabenvielfalt"},
- TypicalHarm: "Unfaelle durch Unaufmerksamkeit, verspaetete Reaktion auf Gefahren",
- RelevantLifecyclePhases: []string{"normal_operation"},
- RecommendedMeasuresDesign: []string{"Automatisierung monotoner Teilaufgaben", "Arbeitsplatzrotation ermoeglichen"},
- RecommendedMeasuresTechnical: []string{"Aufmerksamkeitserkennung", "Regelmaessige Warnmeldungen"},
- RecommendedMeasuresInformation: []string{"Pausenplan erstellen", "Schulung zur Ermuedungserkennung"},
- SuggestedEvidence: []string{"Arbeitsplatzanalyse", "Gefaehrdungsbeurteilung psychische Belastung"},
- RelatedKeywords: []string{"Monotonie", "Aufmerksamkeit", "Ermuedung"},
- IsBuiltin: true,
- TenantID: nil,
- CreatedAt: now,
- },
- {
- ID: hazardUUID("ergonomic", 8),
- Category: "ergonomic",
- SubCategory: "wartungszugang",
- Name: "Ungenuegender Zugang fuer Wartungsarbeiten",
- Description: "Enge oder schwer zugaengliche Wartungsbereiche zwingen das Personal zu gefaehrlichen Koerperhaltungen und erhoehen das Risiko von Verletzungen.",
- DefaultSeverity: 3,
- DefaultProbability: 3,
- DefaultExposure: 3,
- DefaultAvoidance: 3,
- ApplicableComponentTypes: []string{"mechanical", "other"},
- RegulationReferences: []string{"Maschinenverordnung 2023/1230 Anhang I"},
- SuggestedMitigations: mustMarshalJSON([]string{"Wartungsoeffnungen ausreichend dimensionieren", "Wartungsbuehnen und Podeste vorsehen"}),
- TypicalCauses: []string{"Zu kleine Wartungsoeffnungen", "Fehlende Arbeitsbuehnen in der Hoehe", "Wartungspunkte hinter schwer zu demontierenden Verkleidungen"},
- TypicalHarm: "Quetschungen in engen Raeumen, Absturz von erhoehten Wartungspositionen",
- RelevantLifecyclePhases: []string{"maintenance", "fault_finding"},
- RecommendedMeasuresDesign: []string{"Mindestmasse fuer Zugangsoeeffnungen nach ISO 14122", "Wartungspunkte leicht zugaenglich anordnen"},
- RecommendedMeasuresTechnical: []string{"Feste Buehnen und Leitern mit Absturzsicherung", "Schnellverschluesse statt Schraubverbindungen an Wartungsklappen"},
- RecommendedMeasuresInformation: []string{"Wartungszugangsskizze in Betriebsanleitung", "Hinweis auf PSA-Pflicht bei Arbeiten in der Hoehe"},
- SuggestedEvidence: []string{"Zugaenglichkeitspruefung", "Risikobeurteilung"},
- RelatedKeywords: []string{"Wartungszugang", "Zugaenglichkeit", "Wartungsbuehne"},
- IsBuiltin: true,
- TenantID: nil,
- CreatedAt: now,
- },
- // ====================================================================
- // Category: material_environmental (indices 1-8, 8 entries)
- // ====================================================================
- {
- ID: hazardUUID("material_environmental", 1),
- Category: "material_environmental",
- SubCategory: "staubexplosion",
- Name: "Staubexplosion durch Feinpartikel",
- Description: "Feine brennbare Staube koennen in Verbindung mit einer Zuendquelle explosionsartig reagieren und schwere Zerstoerungen verursachen.",
- DefaultSeverity: 5,
- DefaultProbability: 2,
- DefaultExposure: 3,
- DefaultAvoidance: 2,
- ApplicableComponentTypes: []string{"mechanical", "other"},
- RegulationReferences: []string{"Maschinenverordnung 2023/1230 Anhang I"},
- SuggestedMitigations: mustMarshalJSON([]string{"ATEX-konforme Ausfuehrung", "Absauganlage mit Funkenerkennung"}),
- TypicalCauses: []string{"Unzureichende Absaugung", "Staubansammlung in Hohlraeumen", "Elektrische oder mechanische Zuendquelle"},
- TypicalHarm: "Explosion mit toedlichen Verletzungen, schwere Verbrennungen, Gebaeudezerstoerung",
- RelevantLifecyclePhases: []string{"normal_operation", "cleaning"},
- RecommendedMeasuresDesign: []string{"ATEX-konforme Konstruktion", "Druckentlastungsflaechen vorsehen"},
- RecommendedMeasuresTechnical: []string{"Absaugung mit Funkenloeschanlage", "Explosionsunterdrueckung"},
- RecommendedMeasuresInformation: []string{"Ex-Zoneneinteilung kennzeichnen", "Reinigungsplan fuer Staubablagerungen"},
- SuggestedEvidence: []string{"Explosionsschutz-Dokument", "ATEX-Konformitaetserklaerung"},
- RelatedKeywords: []string{"Staubexplosion", "ATEX", "Feinstaub"},
- IsBuiltin: true,
- TenantID: nil,
- CreatedAt: now,
- },
- {
- ID: hazardUUID("material_environmental", 2),
- Category: "material_environmental",
- SubCategory: "rauch_gas",
- Name: "Rauch- und Gasfreisetzung bei Laserschneiden",
- Description: "Beim Laserschneiden entstehen gesundheitsschaedliche Rauche und Gase aus dem verdampften Material, die die Atemwege schaedigen.",
- DefaultSeverity: 4,
- DefaultProbability: 4,
- DefaultExposure: 3,
- DefaultAvoidance: 3,
- ApplicableComponentTypes: []string{"mechanical", "other"},
- RegulationReferences: []string{"Maschinenverordnung 2023/1230 Anhang I"},
- SuggestedMitigations: mustMarshalJSON([]string{"Absaugung direkt am Bearbeitungspunkt", "Filteranlage mit Aktivkohlestufe"}),
- TypicalCauses: []string{"Fehlende Absaugung", "Verarbeitung kunststoffbeschichteter Materialien", "Undichte Maschineneinhausung"},
- TypicalHarm: "Atemwegserkrankungen, Reizung von Augen und Schleimhaeuten",
- RelevantLifecyclePhases: []string{"normal_operation"},
- RecommendedMeasuresDesign: []string{"Geschlossener Bearbeitungsraum mit Absaugung", "Materialauswahl ohne toxische Beschichtungen"},
- RecommendedMeasuresTechnical: []string{"Punktabsaugung mit HEPA-Filter", "Raumluft-Monitoring"},
- RecommendedMeasuresInformation: []string{"Materialfreigabeliste pflegen", "Atemschutz-PSA bei Sondermaterialien"},
- SuggestedEvidence: []string{"Arbeitsplatzmessung Gefahrstoffe", "Risikobeurteilung"},
- RelatedKeywords: []string{"Laserschneiden", "Rauch", "Gefahrstoff"},
- IsBuiltin: true,
- TenantID: nil,
- CreatedAt: now,
- },
- {
- ID: hazardUUID("material_environmental", 3),
- Category: "material_environmental",
- SubCategory: "kuehlschmierstoff",
- Name: "Daempfe aus Kuehlschmierstoffen",
- Description: "Beim Einsatz von Kuehlschmierstoffen entstehen Aerosole und Daempfe, die bei Langzeitexposition zu Haut- und Atemwegserkrankungen fuehren.",
- DefaultSeverity: 3,
- DefaultProbability: 3,
- DefaultExposure: 4,
- DefaultAvoidance: 3,
- ApplicableComponentTypes: []string{"mechanical", "other"},
- RegulationReferences: []string{"Maschinenverordnung 2023/1230 Anhang I"},
- SuggestedMitigations: mustMarshalJSON([]string{"Oelunebelabscheider an der Maschine", "Hautschutzplan fuer Bediener"}),
- TypicalCauses: []string{"Offene Bearbeitungszonen ohne Abschirmung", "Ueberaltertes Kuehlschmiermittel", "Zu hoher Kuehlmitteldurchsatz"},
- TypicalHarm: "Oelakne, Atemwegssensibilisierung, allergische Hautreaktionen",
- RelevantLifecyclePhases: []string{"normal_operation"},
- RecommendedMeasuresDesign: []string{"Geschlossener Bearbeitungsraum", "Minimalmengenschmierung statt Volumenoelstrom"},
- RecommendedMeasuresTechnical: []string{"Oelunebelabscheider", "KSS-Konzentrations- und pH-Ueberwachung"},
- RecommendedMeasuresInformation: []string{"Hautschutz- und Hygieneplan", "KSS-Pflegeanweisung"},
- SuggestedEvidence: []string{"Arbeitsplatz-Gefahrstoffmessung", "Hautschutzplan"},
- RelatedKeywords: []string{"Kuehlschmierstoff", "KSS", "Aerosol"},
- IsBuiltin: true,
- TenantID: nil,
- CreatedAt: now,
- },
- {
- ID: hazardUUID("material_environmental", 4),
- Category: "material_environmental",
- SubCategory: "prozessmedien",
- Name: "Chemische Exposition durch Prozessmedien",
- Description: "Chemisch aggressive Prozessmedien wie Saeuren, Laugen oder Loesemittel koennen bei Hautkontakt oder Einatmen schwere Gesundheitsschaeden verursachen.",
- DefaultSeverity: 4,
- DefaultProbability: 3,
- DefaultExposure: 3,
- DefaultAvoidance: 3,
- ApplicableComponentTypes: []string{"other", "mechanical"},
- RegulationReferences: []string{"Maschinenverordnung 2023/1230 Anhang I"},
- SuggestedMitigations: mustMarshalJSON([]string{"Geschlossene Mediumfuehrung", "Chemikalienschutz-PSA"}),
- TypicalCauses: []string{"Offene Behaelter mit Gefahrstoffen", "Spritzer beim Nachfuellen", "Leckage an Dichtungen"},
- TypicalHarm: "Veraetzungen, Atemwegsschaedigung, Vergiftung",
- RelevantLifecyclePhases: []string{"normal_operation", "maintenance", "cleaning"},
- RecommendedMeasuresDesign: []string{"Geschlossene Kreislaeufe", "Korrosionsbestaendige Materialien"},
- RecommendedMeasuresTechnical: []string{"Notdusche und Augenspueler", "Gaswarnanlage"},
- RecommendedMeasuresInformation: []string{"Sicherheitsdatenblaetter am Arbeitsplatz", "Gefahrstoffunterweisung"},
- SuggestedEvidence: []string{"Gefahrstoffverzeichnis", "Arbeitsplatzmessung"},
- RelatedKeywords: []string{"Gefahrstoff", "Chemikalie", "Prozessmedium"},
- IsBuiltin: true,
- TenantID: nil,
- CreatedAt: now,
- },
- {
- ID: hazardUUID("material_environmental", 5),
- Category: "material_environmental",
- SubCategory: "dichtungsversagen",
- Name: "Leckage von Gefahrstoffen bei Dichtungsversagen",
- Description: "Versagende Dichtungen an Rohrleitungen oder Behaeltern setzen Gefahrstoffe frei und gefaehrden Personal und Umwelt.",
- DefaultSeverity: 4,
- DefaultProbability: 3,
- DefaultExposure: 3,
- DefaultAvoidance: 3,
- ApplicableComponentTypes: []string{"mechanical", "other"},
- RegulationReferences: []string{"Maschinenverordnung 2023/1230 Anhang I"},
- SuggestedMitigations: mustMarshalJSON([]string{"Doppelwandige Behaelter mit Leckagedetektion", "Auffangwannen unter allen Gefahrstoffbereichen"}),
- TypicalCauses: []string{"Alterung der Dichtungsmaterialien", "Chemische Unvertraeglichkeit", "Thermische Ueberbelastung der Dichtung"},
- TypicalHarm: "Gefahrstofffreisetzung mit Vergiftung, Hautschaedigung, Umweltschaden",
- RelevantLifecyclePhases: []string{"normal_operation", "maintenance"},
- RecommendedMeasuresDesign: []string{"Doppelwandige Leitungen", "Chemisch bestaendige Dichtungswerkstoffe"},
- RecommendedMeasuresTechnical: []string{"Leckagesensoren", "Auffangwannen mit Fassungsvermoegen 110%"},
- RecommendedMeasuresInformation: []string{"Dichtungswechselintervalle festlegen", "Notfallplan Gefahrstoffaustritt"},
- SuggestedEvidence: []string{"Dichtheitspruefprotokoll", "Gefahrstoff-Notfallplan"},
- RelatedKeywords: []string{"Leckage", "Dichtung", "Gefahrstoff"},
- IsBuiltin: true,
- TenantID: nil,
- CreatedAt: now,
- },
- {
- ID: hazardUUID("material_environmental", 6),
- Category: "material_environmental",
- SubCategory: "biologische_kontamination",
- Name: "Biologische Kontamination in Lebensmittelmaschinen",
- Description: "In Maschinen der Lebensmittelindustrie koennen sich Mikroorganismen in schwer zu reinigenden Bereichen ansiedeln und das Produkt kontaminieren.",
- DefaultSeverity: 4,
- DefaultProbability: 3,
- DefaultExposure: 3,
- DefaultAvoidance: 3,
- ApplicableComponentTypes: []string{"mechanical", "other"},
- RegulationReferences: []string{"Maschinenverordnung 2023/1230 Anhang I"},
- SuggestedMitigations: mustMarshalJSON([]string{"Hygienic-Design-Konstruktion", "CIP-Reinigungssystem"}),
- TypicalCauses: []string{"Totraeume und Hinterschneidungen in der Konstruktion", "Unzureichende Reinigung", "Poroese Oberflaechen"},
- TypicalHarm: "Lebensmittelkontamination mit Gesundheitsgefaehrdung der Verbraucher",
- RelevantLifecyclePhases: []string{"normal_operation", "cleaning", "maintenance"},
- RecommendedMeasuresDesign: []string{"Hygienic Design nach EHEDG-Richtlinien", "Selbstentleerende Konstruktion"},
- RecommendedMeasuresTechnical: []string{"CIP-Reinigungsanlage", "Oberflaechenguete Ra kleiner 0.8 Mikrometer"},
- RecommendedMeasuresInformation: []string{"Reinigungs- und Desinfektionsplan", "Hygieneschulung des Personals"},
- SuggestedEvidence: []string{"EHEDG-Zertifikat", "Mikrobiologische Abklatschproben"},
- RelatedKeywords: []string{"Hygiene", "Kontamination", "Lebensmittelsicherheit"},
- IsBuiltin: true,
- TenantID: nil,
- CreatedAt: now,
- },
- {
- ID: hazardUUID("material_environmental", 7),
- Category: "material_environmental",
- SubCategory: "elektrostatik",
- Name: "Statische Aufladung in staubhaltiger Umgebung",
- Description: "In staubhaltiger oder explosionsfaehiger Atmosphaere kann eine elektrostatische Entladung als Zuendquelle wirken und eine Explosion oder einen Brand ausloesen.",
- DefaultSeverity: 5,
- DefaultProbability: 2,
- DefaultExposure: 3,
- DefaultAvoidance: 3,
- ApplicableComponentTypes: []string{"mechanical", "electrical", "other"},
- RegulationReferences: []string{"Maschinenverordnung 2023/1230 Anhang I"},
- SuggestedMitigations: mustMarshalJSON([]string{"Erdung aller leitfaehigen Teile", "Ableitfaehige Bodenbeschichtung"}),
- TypicalCauses: []string{"Nicht geerdete Maschinenteile", "Reibung von Kunststoffbaendern", "Niedrige Luftfeuchtigkeit"},
- TypicalHarm: "Zuendung explosionsfaehiger Atmosphaere, Explosion oder Brand",
- RelevantLifecyclePhases: []string{"normal_operation"},
- RecommendedMeasuresDesign: []string{"Leitfaehige Materialien verwenden", "Erdungskonzept fuer alle Komponenten"},
- RecommendedMeasuresTechnical: []string{"Ionisatoren zur Ladungsneutralisation", "Erdungsueberwachung"},
- RecommendedMeasuresInformation: []string{"ESD-Hinweisschilder", "Schulung zur elektrostatischen Gefaehrdung"},
- SuggestedEvidence: []string{"Erdungsmessung", "Explosionsschutz-Dokument"},
- RelatedKeywords: []string{"Elektrostatik", "ESD", "Zuendquelle"},
- IsBuiltin: true,
- TenantID: nil,
- CreatedAt: now,
- },
- {
- ID: hazardUUID("material_environmental", 8),
- Category: "material_environmental",
- SubCategory: "uv_strahlung",
- Name: "UV-Strahlung bei Schweiss- oder Haerteprozessen",
- Description: "Schweissvorgaenge und UV-Haerteprozesse emittieren ultraviolette Strahlung, die Augen und Haut schwer schaedigen kann.",
- DefaultSeverity: 4,
- DefaultProbability: 3,
- DefaultExposure: 3,
- DefaultAvoidance: 3,
- ApplicableComponentTypes: []string{"mechanical", "electrical", "other"},
- RegulationReferences: []string{"Maschinenverordnung 2023/1230 Anhang I"},
- SuggestedMitigations: mustMarshalJSON([]string{"Abschirmung des UV-Bereichs", "Schweissschutzschirm und Schutzkleidung"}),
- TypicalCauses: []string{"Fehlende Abschirmung der UV-Quelle", "Reflexion an glaenzenden Oberflaechen", "Aufenthalt im Strahlungsbereich ohne Schutz"},
- TypicalHarm: "Verblitzen der Augen, Hautverbrennungen, erhoehtes Hautkrebsrisiko bei Langzeitexposition",
- RelevantLifecyclePhases: []string{"normal_operation", "maintenance"},
- RecommendedMeasuresDesign: []string{"Vollstaendige Einhausung der UV-Quelle", "UV-absorbierende Schutzscheiben"},
- RecommendedMeasuresTechnical: []string{"Schweissvorhaenge um den Arbeitsbereich", "UV-Sensor mit Abschaltung"},
- RecommendedMeasuresInformation: []string{"Warnhinweis UV-Strahlung", "PSA-Pflicht: Schweissschutzhelm und Schutzkleidung"},
- SuggestedEvidence: []string{"UV-Strahlungsmessung", "Risikobeurteilung"},
- RelatedKeywords: []string{"UV-Strahlung", "Schweissen", "Strahlenschutz"},
- IsBuiltin: true,
- TenantID: nil,
- CreatedAt: now,
- },
- }
-
- entries = append(entries, iso12100Entries...)
-
- return entries
+ var all []HazardLibraryEntry
+ all = append(all, builtinHazardsAISW()...)
+ all = append(all, builtinHazardsSoftwareHMI()...)
+ all = append(all, builtinHazardsMachineSafety()...)
+ all = append(all, builtinHazardsISO12100Mechanical()...)
+ all = append(all, builtinHazardsISO12100ElectricalThermal()...)
+ all = append(all, builtinHazardsISO12100Pneumatic()...)
+ all = append(all, builtinHazardsISO12100Env()...)
+ return all
}
diff --git a/ai-compliance-sdk/internal/iace/hazard_library_ai_sw.go b/ai-compliance-sdk/internal/iace/hazard_library_ai_sw.go
new file mode 100644
index 0000000..5e1d7f9
--- /dev/null
+++ b/ai-compliance-sdk/internal/iace/hazard_library_ai_sw.go
@@ -0,0 +1,580 @@
+package iace
+
+import "time"
+
+// builtinHazardsAISW returns the initial hazard library entries covering
+// AI/SW/network-related categories: false_classification, timing_error,
+// data_poisoning, model_drift, sensor_spoofing, communication_failure,
+// unauthorized_access, firmware_corruption, safety_boundary_violation,
+// mode_confusion, unintended_bias, update_failure.
+func builtinHazardsAISW() []HazardLibraryEntry {
+ now := time.Now()
+
+ return []HazardLibraryEntry{
+ // ====================================================================
+ // Category: false_classification (4 entries)
+ // ====================================================================
+ {
+ ID: hazardUUID("false_classification", 1),
+ Category: "false_classification",
+ Name: "Falsche Bauteil-Klassifikation durch KI",
+ Description: "Das KI-Modell klassifiziert ein Bauteil fehlerhaft, was zu falscher Weiterverarbeitung oder Montage fuehren kann.",
+ DefaultSeverity: 4,
+ DefaultProbability: 3,
+ ApplicableComponentTypes: []string{"ai_model", "sensor"},
+ RegulationReferences: []string{"EU AI Act Art. 9", "Maschinenverordnung 2023/1230"},
+ SuggestedMitigations: mustMarshalJSON([]string{"Redundante Pruefung", "Konfidenz-Schwellwert"}),
+ IsBuiltin: true,
+ TenantID: nil,
+ CreatedAt: now,
+ },
+ {
+ ID: hazardUUID("false_classification", 2),
+ Category: "false_classification",
+ Name: "Falsche Qualitaetsentscheidung (IO/NIO)",
+ Description: "Fehlerhafte IO/NIO-Entscheidung durch das KI-System fuehrt dazu, dass defekte Teile als gut bewertet oder gute Teile verworfen werden.",
+ DefaultSeverity: 4,
+ DefaultProbability: 3,
+ ApplicableComponentTypes: []string{"ai_model", "software"},
+ RegulationReferences: []string{"EU AI Act Art. 9", "Maschinenverordnung 2023/1230"},
+ SuggestedMitigations: mustMarshalJSON([]string{"Human-in-the-Loop", "Stichproben-Gegenpruefung"}),
+ IsBuiltin: true,
+ TenantID: nil,
+ CreatedAt: now,
+ },
+ {
+ ID: hazardUUID("false_classification", 3),
+ Category: "false_classification",
+ Name: "Fehlklassifikation bei Grenzwertfaellen",
+ Description: "Bauteile nahe an Toleranzgrenzen werden systematisch falsch klassifiziert, da das Modell in Grenzwertbereichen unsicher agiert.",
+ DefaultSeverity: 3,
+ DefaultProbability: 4,
+ ApplicableComponentTypes: []string{"ai_model"},
+ RegulationReferences: []string{"EU AI Act Art. 9", "ISO 13849"},
+ SuggestedMitigations: mustMarshalJSON([]string{"Erweitertes Training", "Grauzone-Eskalation"}),
+ IsBuiltin: true,
+ TenantID: nil,
+ CreatedAt: now,
+ },
+ {
+ ID: hazardUUID("false_classification", 4),
+ Category: "false_classification",
+ Name: "Verwechslung von Bauteiltypen",
+ Description: "Unterschiedliche Bauteiltypen werden vom KI-Modell verwechselt, was zu falscher Montage oder Verarbeitung fuehrt.",
+ DefaultSeverity: 4,
+ DefaultProbability: 2,
+ ApplicableComponentTypes: []string{"ai_model", "sensor"},
+ RegulationReferences: []string{"EU AI Act Art. 9", "Maschinenverordnung 2023/1230"},
+ SuggestedMitigations: mustMarshalJSON([]string{"Barcode-Gegenpruefung", "Doppelte Sensorik"}),
+ IsBuiltin: true,
+ TenantID: nil,
+ CreatedAt: now,
+ },
+
+ // ====================================================================
+ // Category: timing_error (3 entries)
+ // ====================================================================
+ {
+ ID: hazardUUID("timing_error", 1),
+ Category: "timing_error",
+ Name: "Verzoegerte KI-Reaktion in Echtzeitsystem",
+ Description: "Die KI-Inferenz dauert laenger als die zulaessige Echtzeitfrist, was zu verspaeteten Sicherheitsreaktionen fuehrt.",
+ DefaultSeverity: 5,
+ DefaultProbability: 2,
+ ApplicableComponentTypes: []string{"software", "ai_model"},
+ RegulationReferences: []string{"Maschinenverordnung 2023/1230", "ISO 13849", "IEC 62443"},
+ SuggestedMitigations: mustMarshalJSON([]string{"Watchdog-Timer", "Fallback-Steuerung"}),
+ IsBuiltin: true,
+ TenantID: nil,
+ CreatedAt: now,
+ },
+ {
+ ID: hazardUUID("timing_error", 2),
+ Category: "timing_error",
+ Name: "Echtzeit-Verletzung Safety-Loop",
+ Description: "Der sicherheitsgerichtete Regelkreis kann die geforderten Zykluszeiten nicht einhalten, wodurch Sicherheitsfunktionen versagen koennen.",
+ DefaultSeverity: 5,
+ DefaultProbability: 2,
+ ApplicableComponentTypes: []string{"software", "firmware"},
+ RegulationReferences: []string{"ISO 13849", "IEC 61508", "Maschinenverordnung 2023/1230"},
+ SuggestedMitigations: mustMarshalJSON([]string{"Deterministische Ausfuehrung", "WCET-Analyse"}),
+ IsBuiltin: true,
+ TenantID: nil,
+ CreatedAt: now,
+ },
+ {
+ ID: hazardUUID("timing_error", 3),
+ Category: "timing_error",
+ Name: "Timing-Jitter bei Netzwerkkommunikation",
+ Description: "Schwankende Netzwerklatenzen fuehren zu unvorhersehbaren Verzoegerungen in der Datenuebertragung sicherheitsrelevanter Signale.",
+ DefaultSeverity: 3,
+ DefaultProbability: 3,
+ ApplicableComponentTypes: []string{"network", "software"},
+ RegulationReferences: []string{"IEC 62443", "Maschinenverordnung 2023/1230"},
+ SuggestedMitigations: mustMarshalJSON([]string{"TSN-Netzwerk", "Pufferung"}),
+ IsBuiltin: true,
+ TenantID: nil,
+ CreatedAt: now,
+ },
+
+ // ====================================================================
+ // Category: data_poisoning (2 entries)
+ // ====================================================================
+ {
+ ID: hazardUUID("data_poisoning", 1),
+ Category: "data_poisoning",
+ Name: "Manipulierte Trainingsdaten",
+ Description: "Trainingsdaten werden absichtlich oder unbeabsichtigt manipuliert, wodurch das Modell systematisch fehlerhafte Entscheidungen trifft.",
+ DefaultSeverity: 4,
+ DefaultProbability: 2,
+ ApplicableComponentTypes: []string{"ai_model"},
+ RegulationReferences: []string{"EU AI Act Art. 10", "CRA"},
+ SuggestedMitigations: mustMarshalJSON([]string{"Daten-Validierung", "Anomalie-Erkennung"}),
+ IsBuiltin: true,
+ TenantID: nil,
+ CreatedAt: now,
+ },
+ {
+ ID: hazardUUID("data_poisoning", 2),
+ Category: "data_poisoning",
+ Name: "Adversarial Input Angriff",
+ Description: "Gezielte Manipulation von Eingabedaten (z.B. Bilder, Sensorsignale), um das KI-Modell zu taeuschen und Fehlentscheidungen auszuloesen.",
+ DefaultSeverity: 4,
+ DefaultProbability: 2,
+ ApplicableComponentTypes: []string{"ai_model", "sensor"},
+ RegulationReferences: []string{"EU AI Act Art. 15", "CRA", "IEC 62443"},
+ SuggestedMitigations: mustMarshalJSON([]string{"Input-Validation", "Adversarial Training"}),
+ IsBuiltin: true,
+ TenantID: nil,
+ CreatedAt: now,
+ },
+
+ // ====================================================================
+ // Category: model_drift (3 entries)
+ // ====================================================================
+ {
+ ID: hazardUUID("model_drift", 1),
+ Category: "model_drift",
+ Name: "Performance-Degradation durch Concept Drift",
+ Description: "Die statistische Verteilung der Eingabedaten aendert sich ueber die Zeit, wodurch die Modellgenauigkeit schleichend abnimmt.",
+ DefaultSeverity: 3,
+ DefaultProbability: 4,
+ ApplicableComponentTypes: []string{"ai_model"},
+ RegulationReferences: []string{"EU AI Act Art. 9", "EU AI Act Art. 72"},
+ SuggestedMitigations: mustMarshalJSON([]string{"Monitoring-Dashboard", "Automatisches Retraining"}),
+ IsBuiltin: true,
+ TenantID: nil,
+ CreatedAt: now,
+ },
+ {
+ ID: hazardUUID("model_drift", 2),
+ Category: "model_drift",
+ Name: "Data Drift durch veraenderte Umgebung",
+ Description: "Aenderungen in der physischen Umgebung (Beleuchtung, Temperatur, Material) fuehren zu veraenderten Sensordaten und Modellfehlern.",
+ DefaultSeverity: 3,
+ DefaultProbability: 4,
+ ApplicableComponentTypes: []string{"ai_model", "sensor"},
+ RegulationReferences: []string{"EU AI Act Art. 9", "Maschinenverordnung 2023/1230"},
+ SuggestedMitigations: mustMarshalJSON([]string{"Statistische Ueberwachung", "Sensor-Kalibrierung"}),
+ IsBuiltin: true,
+ TenantID: nil,
+ CreatedAt: now,
+ },
+ {
+ ID: hazardUUID("model_drift", 3),
+ Category: "model_drift",
+ Name: "Schleichende Modell-Verschlechterung",
+ Description: "Ohne aktives Monitoring verschlechtert sich die Modellqualitaet ueber Wochen oder Monate unbemerkt.",
+ DefaultSeverity: 3,
+ DefaultProbability: 3,
+ ApplicableComponentTypes: []string{"ai_model"},
+ RegulationReferences: []string{"EU AI Act Art. 9", "EU AI Act Art. 72"},
+ SuggestedMitigations: mustMarshalJSON([]string{"Regelmaessige Evaluierung", "A/B-Testing"}),
+ IsBuiltin: true,
+ TenantID: nil,
+ CreatedAt: now,
+ },
+
+ // ====================================================================
+ // Category: sensor_spoofing (3 entries)
+ // ====================================================================
+ {
+ ID: hazardUUID("sensor_spoofing", 1),
+ Category: "sensor_spoofing",
+ Name: "Kamera-Manipulation / Abdeckung",
+ Description: "Kamerasensoren werden absichtlich oder unbeabsichtigt abgedeckt oder manipuliert, sodass das System auf Basis falscher Bilddaten agiert.",
+ DefaultSeverity: 4,
+ DefaultProbability: 2,
+ ApplicableComponentTypes: []string{"sensor"},
+ RegulationReferences: []string{"IEC 62443", "Maschinenverordnung 2023/1230"},
+ SuggestedMitigations: mustMarshalJSON([]string{"Plausibilitaetspruefung", "Mehrfach-Sensorik"}),
+ IsBuiltin: true,
+ TenantID: nil,
+ CreatedAt: now,
+ },
+ {
+ ID: hazardUUID("sensor_spoofing", 2),
+ Category: "sensor_spoofing",
+ Name: "Sensor-Signal-Injection",
+ Description: "Einspeisung gefaelschter Signale in die Sensorleitungen oder Schnittstellen, um das System gezielt zu manipulieren.",
+ DefaultSeverity: 5,
+ DefaultProbability: 1,
+ ApplicableComponentTypes: []string{"sensor", "network"},
+ RegulationReferences: []string{"IEC 62443", "CRA"},
+ SuggestedMitigations: mustMarshalJSON([]string{"Signalverschluesselung", "Anomalie-Erkennung"}),
+ IsBuiltin: true,
+ TenantID: nil,
+ CreatedAt: now,
+ },
+ {
+ ID: hazardUUID("sensor_spoofing", 3),
+ Category: "sensor_spoofing",
+ Name: "Umgebungsbasierte Sensor-Taeuschung",
+ Description: "Natuerliche oder kuenstliche Umgebungsveraenderungen (Licht, Staub, Vibration) fuehren zu fehlerhaften Sensorwerten.",
+ DefaultSeverity: 3,
+ DefaultProbability: 3,
+ ApplicableComponentTypes: []string{"sensor"},
+ RegulationReferences: []string{"Maschinenverordnung 2023/1230", "ISO 13849"},
+ SuggestedMitigations: mustMarshalJSON([]string{"Sensor-Fusion", "Redundanz"}),
+ IsBuiltin: true,
+ TenantID: nil,
+ CreatedAt: now,
+ },
+
+ // ====================================================================
+ // Category: communication_failure (3 entries)
+ // ====================================================================
+ {
+ ID: hazardUUID("communication_failure", 1),
+ Category: "communication_failure",
+ Name: "Feldbus-Ausfall",
+ Description: "Ausfall des industriellen Feldbusses (z.B. PROFINET, EtherCAT) fuehrt zum Verlust der Kommunikation zwischen Steuerung und Aktorik.",
+ DefaultSeverity: 4,
+ DefaultProbability: 3,
+ ApplicableComponentTypes: []string{"network", "controller"},
+ RegulationReferences: []string{"Maschinenverordnung 2023/1230", "ISO 13849", "IEC 62443"},
+ SuggestedMitigations: mustMarshalJSON([]string{"Redundanter Bus", "Safe-State-Transition"}),
+ IsBuiltin: true,
+ TenantID: nil,
+ CreatedAt: now,
+ },
+ {
+ ID: hazardUUID("communication_failure", 2),
+ Category: "communication_failure",
+ Name: "Cloud-Verbindungsverlust",
+ Description: "Die Verbindung zur Cloud-Infrastruktur bricht ab, wodurch cloud-abhaengige Funktionen (z.B. Modell-Updates, Monitoring) nicht verfuegbar sind.",
+ DefaultSeverity: 3,
+ DefaultProbability: 4,
+ ApplicableComponentTypes: []string{"network", "software"},
+ RegulationReferences: []string{"CRA", "EU AI Act Art. 15"},
+ SuggestedMitigations: mustMarshalJSON([]string{"Offline-Faehigkeit", "Edge-Computing"}),
+ IsBuiltin: true,
+ TenantID: nil,
+ CreatedAt: now,
+ },
+ {
+ ID: hazardUUID("communication_failure", 3),
+ Category: "communication_failure",
+ Name: "Netzwerk-Latenz-Spitzen",
+ Description: "Unkontrollierte Latenzspitzen im Netzwerk fuehren zu Timeouts und verspaeteter Datenlieferung an sicherheitsrelevante Systeme.",
+ DefaultSeverity: 3,
+ DefaultProbability: 3,
+ ApplicableComponentTypes: []string{"network"},
+ RegulationReferences: []string{"IEC 62443", "Maschinenverordnung 2023/1230"},
+ SuggestedMitigations: mustMarshalJSON([]string{"QoS-Konfiguration", "Timeout-Handling"}),
+ IsBuiltin: true,
+ TenantID: nil,
+ CreatedAt: now,
+ },
+
+ // ====================================================================
+ // Category: unauthorized_access (4 entries)
+ // ====================================================================
+ {
+ ID: hazardUUID("unauthorized_access", 1),
+ Category: "unauthorized_access",
+ Name: "Unautorisierter Remote-Zugriff",
+ Description: "Ein Angreifer erlangt ueber das Netzwerk Zugriff auf die Maschinensteuerung und kann sicherheitsrelevante Parameter aendern.",
+ DefaultSeverity: 5,
+ DefaultProbability: 2,
+ ApplicableComponentTypes: []string{"network", "software"},
+ RegulationReferences: []string{"IEC 62443", "CRA", "EU AI Act Art. 15"},
+ SuggestedMitigations: mustMarshalJSON([]string{"VPN", "MFA", "Netzwerksegmentierung"}),
+ IsBuiltin: true,
+ TenantID: nil,
+ CreatedAt: now,
+ },
+ {
+ ID: hazardUUID("unauthorized_access", 2),
+ Category: "unauthorized_access",
+ Name: "Konfigurations-Manipulation",
+ Description: "Sicherheitsrelevante Konfigurationsparameter werden unautorisiert geaendert, z.B. Grenzwerte, Schwellwerte oder Betriebsmodi.",
+ DefaultSeverity: 5,
+ DefaultProbability: 2,
+ ApplicableComponentTypes: []string{"software", "firmware"},
+ RegulationReferences: []string{"IEC 62443", "CRA", "Maschinenverordnung 2023/1230"},
+ SuggestedMitigations: mustMarshalJSON([]string{"Zugriffskontrolle", "Audit-Log"}),
+ IsBuiltin: true,
+ TenantID: nil,
+ CreatedAt: now,
+ },
+ {
+ ID: hazardUUID("unauthorized_access", 3),
+ Category: "unauthorized_access",
+ Name: "Privilege Escalation",
+ Description: "Ein Benutzer oder Prozess erlangt hoehere Berechtigungen als vorgesehen und kann sicherheitskritische Aktionen ausfuehren.",
+ DefaultSeverity: 5,
+ DefaultProbability: 1,
+ ApplicableComponentTypes: []string{"software"},
+ RegulationReferences: []string{"IEC 62443", "CRA"},
+ SuggestedMitigations: mustMarshalJSON([]string{"RBAC", "Least Privilege"}),
+ IsBuiltin: true,
+ TenantID: nil,
+ CreatedAt: now,
+ },
+ {
+ ID: hazardUUID("unauthorized_access", 4),
+ Category: "unauthorized_access",
+ Name: "Supply-Chain-Angriff auf Komponente",
+ Description: "Eine kompromittierte Softwarekomponente oder Firmware wird ueber die Lieferkette eingeschleust und enthaelt Schadcode oder Backdoors.",
+ DefaultSeverity: 5,
+ DefaultProbability: 1,
+ ApplicableComponentTypes: []string{"software", "firmware"},
+ RegulationReferences: []string{"CRA", "IEC 62443", "EU AI Act Art. 15"},
+ SuggestedMitigations: mustMarshalJSON([]string{"SBOM", "Signaturpruefung"}),
+ IsBuiltin: true,
+ TenantID: nil,
+ CreatedAt: now,
+ },
+
+ // ====================================================================
+ // Category: firmware_corruption (3 entries)
+ // ====================================================================
+ {
+ ID: hazardUUID("firmware_corruption", 1),
+ Category: "firmware_corruption",
+ Name: "Update-Abbruch mit inkonsistentem Zustand",
+ Description: "Ein Firmware-Update wird unterbrochen (z.B. Stromausfall), wodurch das System in einem inkonsistenten und potenziell unsicheren Zustand verbleibt.",
+ DefaultSeverity: 5,
+ DefaultProbability: 2,
+ ApplicableComponentTypes: []string{"firmware"},
+ RegulationReferences: []string{"CRA", "Maschinenverordnung 2023/1230"},
+ SuggestedMitigations: mustMarshalJSON([]string{"A/B-Partitioning", "Rollback"}),
+ IsBuiltin: true,
+ TenantID: nil,
+ CreatedAt: now,
+ },
+ {
+ ID: hazardUUID("firmware_corruption", 2),
+ Category: "firmware_corruption",
+ Name: "Rollback-Fehler auf alte Version",
+ Description: "Ein Rollback auf eine aeltere Firmware-Version schlaegt fehl oder fuehrt zu Inkompatibilitaeten mit der aktuellen Hardware-/Softwarekonfiguration.",
+ DefaultSeverity: 4,
+ DefaultProbability: 2,
+ ApplicableComponentTypes: []string{"firmware"},
+ RegulationReferences: []string{"CRA", "Maschinenverordnung 2023/1230"},
+ SuggestedMitigations: mustMarshalJSON([]string{"Versionsmanagement", "Kompatibilitaetspruefung"}),
+ IsBuiltin: true,
+ TenantID: nil,
+ CreatedAt: now,
+ },
+ {
+ ID: hazardUUID("firmware_corruption", 3),
+ Category: "firmware_corruption",
+ Name: "Boot-Chain-Angriff",
+ Description: "Die Bootsequenz wird manipuliert, um unsignierte oder kompromittierte Firmware auszufuehren, was die gesamte Sicherheitsarchitektur untergaebt.",
+ DefaultSeverity: 5,
+ DefaultProbability: 1,
+ ApplicableComponentTypes: []string{"firmware"},
+ RegulationReferences: []string{"CRA", "IEC 62443"},
+ SuggestedMitigations: mustMarshalJSON([]string{"Secure Boot", "TPM"}),
+ IsBuiltin: true,
+ TenantID: nil,
+ CreatedAt: now,
+ },
+
+ // ====================================================================
+ // Category: safety_boundary_violation (4 entries)
+ // ====================================================================
+ {
+ ID: hazardUUID("safety_boundary_violation", 1),
+ Category: "safety_boundary_violation",
+ Name: "Kraft-/Drehmoment-Ueberschreitung",
+ Description: "Aktorische Systeme ueberschreiten die zulaessigen Kraft- oder Drehmomentwerte, was zu Verletzungen oder Maschinenschaeden fuehren kann.",
+ DefaultSeverity: 5,
+ DefaultProbability: 2,
+ ApplicableComponentTypes: []string{"controller", "actuator"},
+ RegulationReferences: []string{"Maschinenverordnung 2023/1230", "ISO 13849", "IEC 62061"},
+ SuggestedMitigations: mustMarshalJSON([]string{"Hardware-Limiter", "SIL-Ueberwachung"}),
+ IsBuiltin: true,
+ TenantID: nil,
+ CreatedAt: now,
+ },
+ {
+ ID: hazardUUID("safety_boundary_violation", 2),
+ Category: "safety_boundary_violation",
+ Name: "Geschwindigkeitsueberschreitung Roboter",
+ Description: "Ein Industrieroboter ueberschreitet die zulaessige Geschwindigkeit, insbesondere bei Mensch-Roboter-Kollaboration.",
+ DefaultSeverity: 5,
+ DefaultProbability: 2,
+ ApplicableComponentTypes: []string{"controller", "software"},
+ RegulationReferences: []string{"Maschinenverordnung 2023/1230", "ISO 13849", "ISO 10218"},
+ SuggestedMitigations: mustMarshalJSON([]string{"Safe Speed Monitoring", "Lichtgitter"}),
+ IsBuiltin: true,
+ TenantID: nil,
+ CreatedAt: now,
+ },
+ {
+ ID: hazardUUID("safety_boundary_violation", 3),
+ Category: "safety_boundary_violation",
+ Name: "Versagen des Safe-State",
+ Description: "Das System kann im Fehlerfall keinen sicheren Zustand einnehmen, da die Sicherheitssteuerung selbst versagt.",
+ DefaultSeverity: 5,
+ DefaultProbability: 1,
+ ApplicableComponentTypes: []string{"controller", "software", "firmware"},
+ RegulationReferences: []string{"Maschinenverordnung 2023/1230", "ISO 13849", "IEC 62061"},
+ SuggestedMitigations: mustMarshalJSON([]string{"Redundante Sicherheitssteuerung", "Diverse Programmierung"}),
+ IsBuiltin: true,
+ TenantID: nil,
+ CreatedAt: now,
+ },
+ {
+ ID: hazardUUID("safety_boundary_violation", 4),
+ Category: "safety_boundary_violation",
+ Name: "Arbeitsraum-Verletzung",
+ Description: "Ein Roboter oder Aktor verlaesst seinen definierten Arbeitsraum und dringt in den Schutzbereich von Personen ein.",
+ DefaultSeverity: 5,
+ DefaultProbability: 2,
+ ApplicableComponentTypes: []string{"controller", "sensor"},
+ RegulationReferences: []string{"Maschinenverordnung 2023/1230", "ISO 13849", "ISO 10218"},
+ SuggestedMitigations: mustMarshalJSON([]string{"Sichere Achsueberwachung", "Schutzzaun-Sensorik"}),
+ IsBuiltin: true,
+ TenantID: nil,
+ CreatedAt: now,
+ },
+
+ // ====================================================================
+ // Category: mode_confusion (3 entries)
+ // ====================================================================
+ {
+ ID: hazardUUID("mode_confusion", 1),
+ Category: "mode_confusion",
+ Name: "Falsche Betriebsart aktiv",
+ Description: "Das System befindet sich in einer unbeabsichtigten Betriebsart (z.B. Automatik statt Einrichtbetrieb), was zu unerwarteten Maschinenbewegungen fuehrt.",
+ DefaultSeverity: 4,
+ DefaultProbability: 3,
+ ApplicableComponentTypes: []string{"hmi", "software"},
+ RegulationReferences: []string{"Maschinenverordnung 2023/1230", "ISO 13849"},
+ SuggestedMitigations: mustMarshalJSON([]string{"Betriebsart-Anzeige", "Schluesselschalter"}),
+ IsBuiltin: true,
+ TenantID: nil,
+ CreatedAt: now,
+ },
+ {
+ ID: hazardUUID("mode_confusion", 2),
+ Category: "mode_confusion",
+ Name: "Wartung/Normal-Verwechslung",
+ Description: "Das System wird im Normalbetrieb gewartet oder der Wartungsmodus wird nicht korrekt verlassen, was zu gefaehrlichen Situationen fuehrt.",
+ DefaultSeverity: 5,
+ DefaultProbability: 2,
+ ApplicableComponentTypes: []string{"hmi", "software"},
+ RegulationReferences: []string{"Maschinenverordnung 2023/1230", "ISO 13849"},
+ SuggestedMitigations: mustMarshalJSON([]string{"Zugangskontrolle", "Sicherheitsverriegelung"}),
+ IsBuiltin: true,
+ TenantID: nil,
+ CreatedAt: now,
+ },
+ {
+ ID: hazardUUID("mode_confusion", 3),
+ Category: "mode_confusion",
+ Name: "Automatik-Eingriff waehrend Handbetrieb",
+ Description: "Das System wechselt waehrend des Handbetriebs unerwartet in den Automatikbetrieb, wodurch eine Person im Gefahrenbereich verletzt werden kann.",
+ DefaultSeverity: 5,
+ DefaultProbability: 2,
+ ApplicableComponentTypes: []string{"software", "controller"},
+ RegulationReferences: []string{"Maschinenverordnung 2023/1230", "ISO 13849"},
+ SuggestedMitigations: mustMarshalJSON([]string{"Exklusive Betriebsarten", "Zustimmtaster"}),
+ IsBuiltin: true,
+ TenantID: nil,
+ CreatedAt: now,
+ },
+
+ // ====================================================================
+ // Category: unintended_bias (2 entries)
+ // ====================================================================
+ {
+ ID: hazardUUID("unintended_bias", 1),
+ Category: "unintended_bias",
+ Name: "Diskriminierende KI-Entscheidung",
+ Description: "Das KI-Modell trifft systematisch diskriminierende Entscheidungen, z.B. bei der Qualitaetsbewertung bestimmter Produktchargen oder Lieferanten.",
+ DefaultSeverity: 3,
+ DefaultProbability: 2,
+ ApplicableComponentTypes: []string{"ai_model"},
+ RegulationReferences: []string{"EU AI Act Art. 10", "EU AI Act Art. 71"},
+ SuggestedMitigations: mustMarshalJSON([]string{"Bias-Testing", "Fairness-Metriken"}),
+ IsBuiltin: true,
+ TenantID: nil,
+ CreatedAt: now,
+ },
+ {
+ ID: hazardUUID("unintended_bias", 2),
+ Category: "unintended_bias",
+ Name: "Verzerrte Trainingsdaten",
+ Description: "Die Trainingsdaten sind nicht repraesentativ und enthalten systematische Verzerrungen, die zu unfairen oder fehlerhaften Modellergebnissen fuehren.",
+ DefaultSeverity: 3,
+ DefaultProbability: 3,
+ ApplicableComponentTypes: []string{"ai_model"},
+ RegulationReferences: []string{"EU AI Act Art. 10", "EU AI Act Art. 71"},
+ SuggestedMitigations: mustMarshalJSON([]string{"Datensatz-Audit", "Ausgewogenes Sampling"}),
+ IsBuiltin: true,
+ TenantID: nil,
+ CreatedAt: now,
+ },
+
+ // ====================================================================
+ // Category: update_failure (3 entries)
+ // ====================================================================
+ {
+ ID: hazardUUID("update_failure", 1),
+ Category: "update_failure",
+ Name: "Unvollstaendiges OTA-Update",
+ Description: "Ein Over-the-Air-Update wird nur teilweise uebertragen oder angewendet, wodurch das System in einem inkonsistenten Zustand verbleibt.",
+ DefaultSeverity: 4,
+ DefaultProbability: 3,
+ ApplicableComponentTypes: []string{"firmware", "software"},
+ RegulationReferences: []string{"CRA", "Maschinenverordnung 2023/1230"},
+ SuggestedMitigations: mustMarshalJSON([]string{"Atomare Updates", "Integritaetspruefung"}),
+ IsBuiltin: true,
+ TenantID: nil,
+ CreatedAt: now,
+ },
+ {
+ ID: hazardUUID("update_failure", 2),
+ Category: "update_failure",
+ Name: "Versionskonflikt nach Update",
+ Description: "Nach einem Update sind Software- und Firmware-Versionen inkompatibel, was zu Fehlfunktionen oder Ausfaellen fuehrt.",
+ DefaultSeverity: 3,
+ DefaultProbability: 3,
+ ApplicableComponentTypes: []string{"software", "firmware"},
+ RegulationReferences: []string{"CRA", "Maschinenverordnung 2023/1230"},
+ SuggestedMitigations: mustMarshalJSON([]string{"Kompatibilitaetsmatrix", "Staging-Tests"}),
+ IsBuiltin: true,
+ TenantID: nil,
+ CreatedAt: now,
+ },
+ {
+ ID: hazardUUID("update_failure", 3),
+ Category: "update_failure",
+ Name: "Unkontrollierter Auto-Update",
+ Description: "Ein automatisches Update wird ohne Genehmigung oder ausserhalb eines Wartungsfensters eingespielt und stoert den laufenden Betrieb.",
+ DefaultSeverity: 4,
+ DefaultProbability: 2,
+ ApplicableComponentTypes: []string{"software"},
+ RegulationReferences: []string{"CRA", "Maschinenverordnung 2023/1230", "IEC 62443"},
+ SuggestedMitigations: mustMarshalJSON([]string{"Update-Genehmigung", "Wartungsfenster"}),
+ IsBuiltin: true,
+ TenantID: nil,
+ CreatedAt: now,
+ },
+ }
+}
diff --git a/ai-compliance-sdk/internal/iace/hazard_library_iso12100_electrical_thermal.go b/ai-compliance-sdk/internal/iace/hazard_library_iso12100_electrical_thermal.go
new file mode 100644
index 0000000..5fd5b9b
--- /dev/null
+++ b/ai-compliance-sdk/internal/iace/hazard_library_iso12100_electrical_thermal.go
@@ -0,0 +1,217 @@
+package iace
+
+import "time"
+
+// builtinHazardsISO12100ElectricalThermal returns ISO 12100 electrical hazard
+// entries (indices 7-10) and thermal hazard entries (indices 5-8).
+func builtinHazardsISO12100ElectricalThermal() []HazardLibraryEntry {
+ now := time.Now()
+ return []HazardLibraryEntry{
+ // ====================================================================
+ // Category: electrical_hazard (indices 7-10, 4 entries)
+ // ====================================================================
+ {
+ ID: hazardUUID("electrical_hazard", 7),
+ Category: "electrical_hazard",
+ SubCategory: "lichtbogen",
+ Name: "Lichtbogengefahr bei Schalthandlungen",
+ Description: "Beim Schalten unter Last kann ein Lichtbogen entstehen, der zu Verbrennungen und Augenschaeden fuehrt.",
+ DefaultSeverity: 5,
+ DefaultProbability: 2,
+ DefaultExposure: 2,
+ DefaultAvoidance: 2,
+ ApplicableComponentTypes: []string{"electrical"},
+ RegulationReferences: []string{"Maschinenverordnung 2023/1230 Anhang I"},
+ SuggestedMitigations: mustMarshalJSON([]string{"Lichtbogenschutzkleidung (PSA)", "Fernbediente Schaltgeraete"}),
+ TypicalCauses: []string{"Schalten unter Last", "Verschmutzte Kontakte", "Fehlbedienung bei Wartung"},
+ TypicalHarm: "Verbrennungen durch Lichtbogen, Augenschaeden, Druckwelle",
+ RelevantLifecyclePhases: []string{"maintenance", "fault_finding"},
+ RecommendedMeasuresDesign: []string{"Lasttrennschalter mit Lichtbogenkammer", "Beruerungssichere Klemmleisten"},
+ RecommendedMeasuresTechnical: []string{"Lichtbogen-Erkennungssystem", "Fernausloesemoeglichkeit"},
+ RecommendedMeasuresInformation: []string{"PSA-Pflicht bei Schalthandlungen", "Schaltbefugnisregelung"},
+ SuggestedEvidence: []string{"Lichtbogenberechnung", "PSA-Ausstattungsnachweis"},
+ RelatedKeywords: []string{"Lichtbogen", "Schalthandlung", "Arc Flash"},
+ IsBuiltin: true,
+ TenantID: nil,
+ CreatedAt: now,
+ },
+ {
+ ID: hazardUUID("electrical_hazard", 8),
+ Category: "electrical_hazard",
+ SubCategory: "ueberstrom",
+ Name: "Ueberstrom durch Kurzschluss",
+ Description: "Ein Kurzschluss kann zu extrem hohen Stroemen fuehren, die Leitungen ueberhitzen, Braende ausloesen und Bauteile zerstoeren.",
+ DefaultSeverity: 4,
+ DefaultProbability: 2,
+ DefaultExposure: 2,
+ DefaultAvoidance: 3,
+ ApplicableComponentTypes: []string{"electrical"},
+ RegulationReferences: []string{"Maschinenverordnung 2023/1230 Anhang I"},
+ SuggestedMitigations: mustMarshalJSON([]string{"Selektive Absicherung mit Schmelzsicherungen", "Kurzschlussberechnung und Abschaltzeitnachweis"}),
+ TypicalCauses: []string{"Beschaedigte Leitungsisolierung", "Feuchtigkeitseintritt", "Fehlerhafte Verdrahtung"},
+ TypicalHarm: "Brandgefahr, Zerstoerung elektrischer Betriebsmittel",
+ RelevantLifecyclePhases: []string{"normal_operation", "maintenance", "installation"},
+ RecommendedMeasuresDesign: []string{"Kurzschlussfeste Dimensionierung der Leitungen", "Selektive Schutzkoordination"},
+ RecommendedMeasuresTechnical: []string{"Leitungsschutzschalter", "Fehlerstrom-Schutzeinrichtung"},
+ RecommendedMeasuresInformation: []string{"Stromlaufplan aktuell halten", "Prueffristen fuer elektrische Anlage"},
+ SuggestedEvidence: []string{"Kurzschlussberechnung", "Pruefprotokoll nach DGUV V3"},
+ RelatedKeywords: []string{"Kurzschluss", "Ueberstrom", "Leitungsschutz"},
+ IsBuiltin: true,
+ TenantID: nil,
+ CreatedAt: now,
+ },
+ {
+ ID: hazardUUID("electrical_hazard", 9),
+ Category: "electrical_hazard",
+ SubCategory: "erdungsfehler",
+ Name: "Erdungsfehler im Schutzleitersystem",
+ Description: "Ein unterbrochener oder fehlerhafter Schutzleiter verhindert die sichere Ableitung von Fehlerstroemen und macht Gehaeuse spannungsfuehrend.",
+ DefaultSeverity: 5,
+ DefaultProbability: 2,
+ DefaultExposure: 3,
+ DefaultAvoidance: 2,
+ ApplicableComponentTypes: []string{"electrical"},
+ RegulationReferences: []string{"Maschinenverordnung 2023/1230 Anhang I"},
+ SuggestedMitigations: mustMarshalJSON([]string{"Regelmaessige Schutzleiterpruefung", "Fehlerstrom-Schutzschalter als Zusatzmassnahme"}),
+ TypicalCauses: []string{"Lose Schutzleiterklemme", "Korrosion an Erdungspunkten", "Vergessener Schutzleiteranschluss nach Wartung"},
+ TypicalHarm: "Elektrischer Schlag bei Beruehrung des Maschinengehaeuses",
+ RelevantLifecyclePhases: []string{"normal_operation", "maintenance", "installation"},
+ RecommendedMeasuresDesign: []string{"Redundante Schutzleiteranschluesse", "Schutzleiter-Monitoring"},
+ RecommendedMeasuresTechnical: []string{"RCD-Schutzschalter 30 mA", "Isolationsueberwachung"},
+ RecommendedMeasuresInformation: []string{"Pruefplaketten an Schutzleiterpunkten", "Prueffrist 12 Monate"},
+ SuggestedEvidence: []string{"Schutzleitermessung", "Pruefprotokoll DGUV V3"},
+ RelatedKeywords: []string{"Schutzleiter", "Erdung", "Fehlerstrom"},
+ IsBuiltin: true,
+ TenantID: nil,
+ CreatedAt: now,
+ },
+ {
+ ID: hazardUUID("electrical_hazard", 10),
+ Category: "electrical_hazard",
+ SubCategory: "isolationsversagen",
+ Name: "Isolationsversagen in Hochspannungsbereich",
+ Description: "Alterung, Verschmutzung oder mechanische Beschaedigung der Isolierung in Hochspannungsbereichen kann zu Spannungsueberschlaegen und Koerperdurchstroemung fuehren.",
+ DefaultSeverity: 5,
+ DefaultProbability: 2,
+ DefaultExposure: 2,
+ DefaultAvoidance: 2,
+ ApplicableComponentTypes: []string{"electrical"},
+ RegulationReferences: []string{"Maschinenverordnung 2023/1230 Anhang I"},
+ SuggestedMitigations: mustMarshalJSON([]string{"Isolationswiderstandsmessung", "Spannungsfeste Einhausung"}),
+ TypicalCauses: []string{"Alterung der Isolierstoffe", "Mechanische Beschaedigung", "Verschmutzung und Feuchtigkeit"},
+ TypicalHarm: "Toedlicher Stromschlag, Verbrennungen durch Spannungsueberschlag",
+ RelevantLifecyclePhases: []string{"normal_operation", "maintenance"},
+ RecommendedMeasuresDesign: []string{"Verstaerkte Isolierung in kritischen Bereichen", "Luftstrecken und Kriechstrecken einhalten"},
+ RecommendedMeasuresTechnical: []string{"Isolationsueberwachungsgeraet", "Verriegelter Zugang zum Hochspannungsbereich"},
+ RecommendedMeasuresInformation: []string{"Hochspannungswarnung", "Zutrittsregelung fuer Elektrofachkraefte"},
+ SuggestedEvidence: []string{"Isolationsmessprotokoll", "Pruefbericht Hochspannungsbereich"},
+ RelatedKeywords: []string{"Isolation", "Hochspannung", "Durchschlag"},
+ IsBuiltin: true,
+ TenantID: nil,
+ CreatedAt: now,
+ },
+ // ====================================================================
+ // Category: thermal_hazard (indices 5-8, 4 entries)
+ // ====================================================================
+ {
+ ID: hazardUUID("thermal_hazard", 5),
+ Category: "thermal_hazard",
+ SubCategory: "kaeltekontakt",
+ Name: "Kontakt mit kalten Oberflaechen (Kryotechnik)",
+ Description: "In kryotechnischen Anlagen oder Kuehlsystemen koennen extrem kalte Oberflaechen bei Beruehrung Kaelteverbrennungen verursachen.",
+ DefaultSeverity: 4,
+ DefaultProbability: 2,
+ DefaultExposure: 2,
+ DefaultAvoidance: 3,
+ ApplicableComponentTypes: []string{"mechanical", "other"},
+ RegulationReferences: []string{"Maschinenverordnung 2023/1230 Anhang I"},
+ SuggestedMitigations: mustMarshalJSON([]string{"Isolierung kalter Oberflaechen", "Kaelteschutzhandschuhe"}),
+ TypicalCauses: []string{"Fehlende Isolierung an Kryoleitungen", "Beruehrung tiefgekuehlter Bauteile", "Defekte Kaelteisolierung"},
+ TypicalHarm: "Kaelteverbrennungen an Haenden und Fingern",
+ RelevantLifecyclePhases: []string{"normal_operation", "maintenance"},
+ RecommendedMeasuresDesign: []string{"Isolierung aller kalten Oberflaechen im Zugriffsbereich", "Abstandshalter zu Kryoleitungen"},
+ RecommendedMeasuresTechnical: []string{"Temperaturwarnung bei kritischen Oberflaechentemperaturen"},
+ RecommendedMeasuresInformation: []string{"Warnhinweis Kaeltegefahr", "PSA-Pflicht Kaelteschutz"},
+ SuggestedEvidence: []string{"Oberflaechentemperaturmessung", "Risikobeurteilung"},
+ RelatedKeywords: []string{"Kryotechnik", "Kaelte", "Kaelteverbrennung"},
+ IsBuiltin: true,
+ TenantID: nil,
+ CreatedAt: now,
+ },
+ {
+ ID: hazardUUID("thermal_hazard", 6),
+ Category: "thermal_hazard",
+ SubCategory: "waermestrahlung",
+ Name: "Waermestrahlung von Hochtemperaturprozessen",
+ Description: "Oefen, Giessereianlagen oder Waermebehandlungsprozesse emittieren intensive Waermestrahlung, die auch ohne direkten Kontakt zu Verbrennungen fuehren kann.",
+ DefaultSeverity: 4,
+ DefaultProbability: 3,
+ DefaultExposure: 3,
+ DefaultAvoidance: 3,
+ ApplicableComponentTypes: []string{"mechanical", "other"},
+ RegulationReferences: []string{"Maschinenverordnung 2023/1230 Anhang I"},
+ SuggestedMitigations: mustMarshalJSON([]string{"Waermeschutzschilder", "Hitzeschutzkleidung"}),
+ TypicalCauses: []string{"Offene Ofentuer bei Beschickung", "Fehlende Abschirmung", "Langzeitexposition in der Naehe von Waermequellen"},
+ TypicalHarm: "Hautverbrennungen durch Waermestrahlung, Hitzschlag",
+ RelevantLifecyclePhases: []string{"normal_operation", "setup"},
+ RecommendedMeasuresDesign: []string{"Waermedaemmung und Strahlungsschilde", "Automatische Beschickung statt manueller"},
+ RecommendedMeasuresTechnical: []string{"Waermestrahlung-Sensor mit Warnung", "Luftschleier vor Ofenoeeffnungen"},
+ RecommendedMeasuresInformation: []string{"Maximalaufenthaltsdauer festlegen", "Hitzeschutz-PSA vorschreiben"},
+ SuggestedEvidence: []string{"Waermestrahlungsmessung am Arbeitsplatz", "Risikobeurteilung"},
+ RelatedKeywords: []string{"Waermestrahlung", "Ofen", "Hitzeschutz"},
+ IsBuiltin: true,
+ TenantID: nil,
+ CreatedAt: now,
+ },
+ {
+ ID: hazardUUID("thermal_hazard", 7),
+ Category: "thermal_hazard",
+ SubCategory: "brandgefahr",
+ Name: "Brandgefahr durch ueberhitzte Antriebe",
+ Description: "Ueberlastete oder schlecht gekuehlte Elektromotoren und Antriebe koennen sich so stark erhitzen, dass umgebende Materialien entzuendet werden.",
+ DefaultSeverity: 5,
+ DefaultProbability: 2,
+ DefaultExposure: 3,
+ DefaultAvoidance: 3,
+ ApplicableComponentTypes: []string{"actuator", "electrical"},
+ RegulationReferences: []string{"Maschinenverordnung 2023/1230 Anhang I"},
+ SuggestedMitigations: mustMarshalJSON([]string{"Temperatursensor am Motor", "Thermischer Motorschutz"}),
+ TypicalCauses: []string{"Dauerbetrieb ueber Nennlast", "Blockierter Kuehlluftstrom", "Defektes Motorlager erhoecht Reibung"},
+ TypicalHarm: "Brand mit Sachschaeden und Personengefaehrdung durch Rauchentwicklung",
+ RelevantLifecyclePhases: []string{"normal_operation"},
+ RecommendedMeasuresDesign: []string{"Thermische Motorschutzdimensionierung", "Brandschottung um Antriebsbereich"},
+ RecommendedMeasuresTechnical: []string{"PTC-Temperaturfuehler im Motor", "Rauchmelder im Antriebsbereich"},
+ RecommendedMeasuresInformation: []string{"Wartungsintervalle fuer Kuehlluftwege", "Brandschutzordnung"},
+ SuggestedEvidence: []string{"Temperaturmessung unter Last", "Brandschutzkonzept"},
+ RelatedKeywords: []string{"Motorueberhitzung", "Brand", "Thermischer Schutz"},
+ IsBuiltin: true,
+ TenantID: nil,
+ CreatedAt: now,
+ },
+ {
+ ID: hazardUUID("thermal_hazard", 8),
+ Category: "thermal_hazard",
+ SubCategory: "heisse_fluessigkeiten",
+ Name: "Verbrennungsgefahr durch heisse Fluessigkeiten",
+ Description: "Heisse Prozessfluessigkeiten, Kuehlmittel oder Dampf koennen bei Leckage oder beim Oeffnen von Verschluessen Verbruehungen verursachen.",
+ DefaultSeverity: 4,
+ DefaultProbability: 3,
+ DefaultExposure: 3,
+ DefaultAvoidance: 3,
+ ApplicableComponentTypes: []string{"mechanical", "other"},
+ RegulationReferences: []string{"Maschinenverordnung 2023/1230 Anhang I"},
+ SuggestedMitigations: mustMarshalJSON([]string{"Druckentlastung vor dem Oeffnen", "Spritzschutz an Leitungsverbindungen"}),
+ TypicalCauses: []string{"Oeffnen von Verschluessen unter Druck", "Schlauchbruch bei heissem Medium", "Spritzer beim Nachfuellen"},
+ TypicalHarm: "Verbruehungen an Haut und Augen",
+ RelevantLifecyclePhases: []string{"normal_operation", "maintenance"},
+ RecommendedMeasuresDesign: []string{"Druckentlastungsventil vor Verschluss", "Isolierte Leitungsfuehrung"},
+ RecommendedMeasuresTechnical: []string{"Temperaturanzeige an kritischen Punkten", "Auffangwannen unter Leitungsverbindungen"},
+ RecommendedMeasuresInformation: []string{"Warnhinweis heisse Fluessigkeit", "Abkuehlprozedur in Betriebsanweisung"},
+ SuggestedEvidence: []string{"Temperaturmessung am Austritt", "Risikobeurteilung"},
+ RelatedKeywords: []string{"Verbruehung", "Heisse Fluessigkeit", "Dampf"},
+ IsBuiltin: true,
+ TenantID: nil,
+ CreatedAt: now,
+ },
+ }
+}
diff --git a/ai-compliance-sdk/internal/iace/hazard_library_iso12100_env.go b/ai-compliance-sdk/internal/iace/hazard_library_iso12100_env.go
new file mode 100644
index 0000000..ebb4dc4
--- /dev/null
+++ b/ai-compliance-sdk/internal/iace/hazard_library_iso12100_env.go
@@ -0,0 +1,416 @@
+package iace
+
+import "time"
+
+// builtinHazardsISO12100Env returns ISO 12100 noise, vibration, ergonomic
+// and material/environmental hazard entries.
+func builtinHazardsISO12100Env() []HazardLibraryEntry {
+ now := time.Now()
+ return []HazardLibraryEntry{
+ // Category: ergonomic (indices 1-8, 8 entries)
+ // ====================================================================
+ {
+ ID: hazardUUID("ergonomic", 1),
+ Category: "ergonomic",
+ SubCategory: "fehlbedienung",
+ Name: "Fehlbedienung durch unguenstige Anordnung von Bedienelementen",
+ Description: "Ungluecklich platzierte oder schlecht beschriftete Bedienelemente erhoehen das Risiko von Fehlbedienungen, die sicherheitskritische Maschinenbewegungen ausloesen.",
+ DefaultSeverity: 4,
+ DefaultProbability: 3,
+ DefaultExposure: 4,
+ DefaultAvoidance: 3,
+ ApplicableComponentTypes: []string{"controller", "sensor"},
+ RegulationReferences: []string{"Maschinenverordnung 2023/1230 Anhang I"},
+ SuggestedMitigations: mustMarshalJSON([]string{"Ergonomische Anordnung nach ISO 9355", "Eindeutige Beschriftung und Farbcodierung"}),
+ TypicalCauses: []string{"Nicht-intuitive Anordnung der Schalter", "Fehlende oder unlesbare Beschriftung", "Zu geringer Abstand zwischen Bedienelementen"},
+ TypicalHarm: "Verletzungen durch unbeabsichtigte Maschinenaktionen nach Fehlbedienung",
+ RelevantLifecyclePhases: []string{"normal_operation", "setup"},
+ RecommendedMeasuresDesign: []string{"Bedienelemente nach ISO 9355 anordnen", "Farbcodierung und Symbolik nach IEC 60073"},
+ RecommendedMeasuresTechnical: []string{"Bestaetigung fuer kritische Aktionen", "Abgedeckte Schalter fuer Gefahrenfunktionen"},
+ RecommendedMeasuresInformation: []string{"Bedienerhandbuch mit Bilddarstellungen", "Schulung der Bediener"},
+ SuggestedEvidence: []string{"Usability-Test des Bedienfeldes", "Risikobeurteilung"},
+ RelatedKeywords: []string{"Bedienelemente", "Fehlbedienung", "Ergonomie"},
+ IsBuiltin: true,
+ TenantID: nil,
+ CreatedAt: now,
+ },
+ {
+ ID: hazardUUID("ergonomic", 2),
+ Category: "ergonomic",
+ SubCategory: "zwangshaltung",
+ Name: "Zwangshaltung bei Beschickungsvorgaengen",
+ Description: "Unglueckliche Koerperhaltungen beim manuellen Beladen oder Entnehmen von Werkstuecken fuehren zu muskuloskeletalen Beschwerden.",
+ DefaultSeverity: 3,
+ DefaultProbability: 4,
+ DefaultExposure: 4,
+ DefaultAvoidance: 3,
+ ApplicableComponentTypes: []string{"mechanical", "other"},
+ RegulationReferences: []string{"Maschinenverordnung 2023/1230 Anhang I"},
+ SuggestedMitigations: mustMarshalJSON([]string{"Hoehenverstellbare Beschickungsoeffnung", "Automatische Materialzufuhr"}),
+ TypicalCauses: []string{"Beschickungsoeffnung in unguenstiger Hoehe", "Grosse Greifentfernung", "Wiederholte Drehbewegungen des Rumpfes"},
+ TypicalHarm: "Rueckenbeschwerden, Schulter-Arm-Syndrom, chronische Gelenkschmerzen",
+ RelevantLifecyclePhases: []string{"normal_operation"},
+ RecommendedMeasuresDesign: []string{"Beschickungshoehe zwischen 60 und 120 cm", "Kurze Greifwege"},
+ RecommendedMeasuresTechnical: []string{"Hoehenverstellbare Arbeitstische", "Hebehilfen und Manipulatoren"},
+ RecommendedMeasuresInformation: []string{"Ergonomie-Schulung", "Hinweise zur richtigen Koerperhaltung"},
+ SuggestedEvidence: []string{"Ergonomische Arbeitsplatzanalyse", "Gefaehrdungsbeurteilung"},
+ RelatedKeywords: []string{"Zwangshaltung", "Beschickung", "Muskel-Skelett"},
+ IsBuiltin: true,
+ TenantID: nil,
+ CreatedAt: now,
+ },
+ {
+ ID: hazardUUID("ergonomic", 3),
+ Category: "ergonomic",
+ SubCategory: "manuelle_handhabung",
+ Name: "Koerperliche Ueberforderung durch manuelle Handhabung",
+ Description: "Schwere Lasten muessen manuell gehoben, getragen oder verschoben werden, was zu akuten Verletzungen oder chronischen Schaeden fuehrt.",
+ DefaultSeverity: 3,
+ DefaultProbability: 4,
+ DefaultExposure: 4,
+ DefaultAvoidance: 3,
+ ApplicableComponentTypes: []string{"mechanical", "other"},
+ RegulationReferences: []string{"Maschinenverordnung 2023/1230 Anhang I"},
+ SuggestedMitigations: mustMarshalJSON([]string{"Technische Hebehilfen bereitstellen", "Gewichtsgrenze fuer manuelles Heben festlegen"}),
+ TypicalCauses: []string{"Fehlende Hebehilfen", "Zu schwere Einzelteile", "Haeufiges Heben ueber Schulterhoehe"},
+ TypicalHarm: "Bandscheibenvorfall, Rueckenverletzungen, Ueberanstrengung",
+ RelevantLifecyclePhases: []string{"normal_operation", "maintenance", "setup"},
+ RecommendedMeasuresDesign: []string{"Bauteile unter 15 kg fuer manuelles Handling", "Hebevorrichtungen integrieren"},
+ RecommendedMeasuresTechnical: []string{"Kran oder Hebezeug am Arbeitsplatz", "Vakuumheber fuer Platten"},
+ RecommendedMeasuresInformation: []string{"Hebebelastungstabelle aushangen", "Unterweisung in Hebetechnik"},
+ SuggestedEvidence: []string{"Lastenhandhabungsbeurteilung", "Risikobeurteilung"},
+ RelatedKeywords: []string{"Heben", "Lastenhandhabung", "Ueberanstrengung"},
+ IsBuiltin: true,
+ TenantID: nil,
+ CreatedAt: now,
+ },
+ {
+ ID: hazardUUID("ergonomic", 4),
+ Category: "ergonomic",
+ SubCategory: "verwechslung",
+ Name: "Verwechslungsgefahr bei gleichartigen Bedienelementen",
+ Description: "Baugleiche, nicht unterscheidbare Taster oder Schalter koennen verwechselt werden, was zu unbeabsichtigten und gefaehrlichen Maschinenaktionen fuehrt.",
+ DefaultSeverity: 4,
+ DefaultProbability: 3,
+ DefaultExposure: 4,
+ DefaultAvoidance: 4,
+ ApplicableComponentTypes: []string{"controller", "sensor"},
+ RegulationReferences: []string{"Maschinenverordnung 2023/1230 Anhang I"},
+ SuggestedMitigations: mustMarshalJSON([]string{"Unterschiedliche Formen und Farben fuer verschiedene Funktionen", "Beschriftung in Klartext"}),
+ TypicalCauses: []string{"Identische Tasterform fuer unterschiedliche Funktionen", "Fehlende Beschriftung", "Schlechte Beleuchtung am Bedienfeld"},
+ TypicalHarm: "Unbeabsichtigte Maschinenaktionen mit Verletzungsgefahr",
+ RelevantLifecyclePhases: []string{"normal_operation", "setup"},
+ RecommendedMeasuresDesign: []string{"Form- und Farbcodierung nach IEC 60073", "Raeumliche Gruppierung nach Funktionsbereichen"},
+ RecommendedMeasuresTechnical: []string{"Beleuchtetes Bedienfeld", "Haptisch unterscheidbare Taster"},
+ RecommendedMeasuresInformation: []string{"Bedienfeldplan am Arbeitsplatz aushangen", "Einweisung neuer Bediener"},
+ SuggestedEvidence: []string{"Usability-Bewertung", "Risikobeurteilung"},
+ RelatedKeywords: []string{"Verwechslung", "Taster", "Bedienpanel"},
+ IsBuiltin: true,
+ TenantID: nil,
+ CreatedAt: now,
+ },
+ {
+ ID: hazardUUID("ergonomic", 5),
+ Category: "ergonomic",
+ SubCategory: "sichtbehinderung",
+ Name: "Sichtbehinderung des Gefahrenbereichs vom Bedienplatz",
+ Description: "Vom Bedienplatz aus ist der Gefahrenbereich nicht vollstaendig einsehbar, sodass der Bediener Personen im Gefahrenbereich nicht erkennen kann.",
+ DefaultSeverity: 5,
+ DefaultProbability: 3,
+ DefaultExposure: 4,
+ DefaultAvoidance: 2,
+ ApplicableComponentTypes: []string{"controller", "sensor", "mechanical"},
+ RegulationReferences: []string{"Maschinenverordnung 2023/1230 Anhang I"},
+ SuggestedMitigations: mustMarshalJSON([]string{"Kameraueberwachung des Gefahrenbereichs", "Sicherheits-Laserscanner"}),
+ TypicalCauses: []string{"Verdeckte Sicht durch Maschinenteile", "Bedienplatz zu weit vom Arbeitsbereich", "Fehlende Spiegel oder Kameras"},
+ TypicalHarm: "Schwere Verletzungen von Personen im nicht einsehbaren Gefahrenbereich",
+ RelevantLifecyclePhases: []string{"normal_operation", "setup"},
+ RecommendedMeasuresDesign: []string{"Bedienplatz mit direkter Sicht auf Gefahrenbereich", "Transparente Schutzeinrichtungen"},
+ RecommendedMeasuresTechnical: []string{"Kamerasystem mit Monitor am Bedienplatz", "Sicherheits-Laserscanner"},
+ RecommendedMeasuresInformation: []string{"Hinweis auf toten Winkel", "Anlaufwarnung vor Maschinenbewegung"},
+ SuggestedEvidence: []string{"Sichtfeldanalyse vom Bedienplatz", "Risikobeurteilung"},
+ RelatedKeywords: []string{"Sichtfeld", "Toter Winkel", "Bedienplatz"},
+ IsBuiltin: true,
+ TenantID: nil,
+ CreatedAt: now,
+ },
+ {
+ ID: hazardUUID("ergonomic", 6),
+ Category: "ergonomic",
+ SubCategory: "griffgestaltung",
+ Name: "Unergonomische Griffgestaltung von Handwerkzeugen",
+ Description: "Schlecht geformte Griffe an handgefuehrten Werkzeugen und Hebeln fuehren zu Ermuedung, verminderter Griffkraft und erhoehtem Unfallrisiko.",
+ DefaultSeverity: 3,
+ DefaultProbability: 3,
+ DefaultExposure: 4,
+ DefaultAvoidance: 4,
+ ApplicableComponentTypes: []string{"mechanical", "other"},
+ RegulationReferences: []string{"Maschinenverordnung 2023/1230 Anhang I"},
+ SuggestedMitigations: mustMarshalJSON([]string{"Ergonomische Griffformen nach EN 894", "Rutschfeste Griffoberflaechen"}),
+ TypicalCauses: []string{"Zu duenner oder zu dicker Griff", "Glatte Griffoberflaeche", "Scharfe Kanten am Griff"},
+ TypicalHarm: "Sehnenscheidenentzuendung, Abrutschen mit Folgeverletzung",
+ RelevantLifecyclePhases: []string{"normal_operation", "maintenance"},
+ RecommendedMeasuresDesign: []string{"Griffdurchmesser 30-45 mm", "Anatomisch geformte Griffe"},
+ RecommendedMeasuresTechnical: []string{"Rutschfeste Beschichtung", "Handgelenkschlaufe an Handwerkzeugen"},
+ RecommendedMeasuresInformation: []string{"Auswahl ergonomischer Werkzeuge dokumentieren", "Arbeitsmedizinische Beratung"},
+ SuggestedEvidence: []string{"Ergonomische Werkzeugbewertung", "Risikobeurteilung"},
+ RelatedKeywords: []string{"Griff", "Handwerkzeug", "Ergonomie"},
+ IsBuiltin: true,
+ TenantID: nil,
+ CreatedAt: now,
+ },
+ {
+ ID: hazardUUID("ergonomic", 7),
+ Category: "ergonomic",
+ SubCategory: "monotonie",
+ Name: "Monotone Taetigkeit fuehrt zu Aufmerksamkeitsverlust",
+ Description: "Langandauernde, sich wiederholende Taetigkeiten ohne Abwechslung vermindern die Aufmerksamkeit des Bedieners und erhoehen die Unfallgefahr.",
+ DefaultSeverity: 4,
+ DefaultProbability: 3,
+ DefaultExposure: 4,
+ DefaultAvoidance: 3,
+ ApplicableComponentTypes: []string{"other"},
+ RegulationReferences: []string{"Maschinenverordnung 2023/1230 Anhang I"},
+ SuggestedMitigations: mustMarshalJSON([]string{"Aufgabenwechsel organisieren", "Regelmaessige Pausen einplanen"}),
+ TypicalCauses: []string{"Gleichfoermige Wiederholtaetigkeit", "Fehlende Pausenregelung", "Mangelnde Aufgabenvielfalt"},
+ TypicalHarm: "Unfaelle durch Unaufmerksamkeit, verspaetete Reaktion auf Gefahren",
+ RelevantLifecyclePhases: []string{"normal_operation"},
+ RecommendedMeasuresDesign: []string{"Automatisierung monotoner Teilaufgaben", "Arbeitsplatzrotation ermoeglichen"},
+ RecommendedMeasuresTechnical: []string{"Aufmerksamkeitserkennung", "Regelmaessige Warnmeldungen"},
+ RecommendedMeasuresInformation: []string{"Pausenplan erstellen", "Schulung zur Ermuedungserkennung"},
+ SuggestedEvidence: []string{"Arbeitsplatzanalyse", "Gefaehrdungsbeurteilung psychische Belastung"},
+ RelatedKeywords: []string{"Monotonie", "Aufmerksamkeit", "Ermuedung"},
+ IsBuiltin: true,
+ TenantID: nil,
+ CreatedAt: now,
+ },
+ {
+ ID: hazardUUID("ergonomic", 8),
+ Category: "ergonomic",
+ SubCategory: "wartungszugang",
+ Name: "Ungenuegender Zugang fuer Wartungsarbeiten",
+ Description: "Enge oder schwer zugaengliche Wartungsbereiche zwingen das Personal zu gefaehrlichen Koerperhaltungen und erhoehen das Risiko von Verletzungen.",
+ DefaultSeverity: 3,
+ DefaultProbability: 3,
+ DefaultExposure: 3,
+ DefaultAvoidance: 3,
+ ApplicableComponentTypes: []string{"mechanical", "other"},
+ RegulationReferences: []string{"Maschinenverordnung 2023/1230 Anhang I"},
+ SuggestedMitigations: mustMarshalJSON([]string{"Wartungsoeffnungen ausreichend dimensionieren", "Wartungsbuehnen und Podeste vorsehen"}),
+ TypicalCauses: []string{"Zu kleine Wartungsoeffnungen", "Fehlende Arbeitsbuehnen in der Hoehe", "Wartungspunkte hinter schwer zu demontierenden Verkleidungen"},
+ TypicalHarm: "Quetschungen in engen Raeumen, Absturz von erhoehten Wartungspositionen",
+ RelevantLifecyclePhases: []string{"maintenance", "fault_finding"},
+ RecommendedMeasuresDesign: []string{"Mindestmasse fuer Zugangsoeeffnungen nach ISO 14122", "Wartungspunkte leicht zugaenglich anordnen"},
+ RecommendedMeasuresTechnical: []string{"Feste Buehnen und Leitern mit Absturzsicherung", "Schnellverschluesse statt Schraubverbindungen an Wartungsklappen"},
+ RecommendedMeasuresInformation: []string{"Wartungszugangsskizze in Betriebsanleitung", "Hinweis auf PSA-Pflicht bei Arbeiten in der Hoehe"},
+ SuggestedEvidence: []string{"Zugaenglichkeitspruefung", "Risikobeurteilung"},
+ RelatedKeywords: []string{"Wartungszugang", "Zugaenglichkeit", "Wartungsbuehne"},
+ IsBuiltin: true,
+ TenantID: nil,
+ CreatedAt: now,
+ },
+ // ====================================================================
+ // Category: material_environmental (indices 1-8, 8 entries)
+ // ====================================================================
+ {
+ ID: hazardUUID("material_environmental", 1),
+ Category: "material_environmental",
+ SubCategory: "staubexplosion",
+ Name: "Staubexplosion durch Feinpartikel",
+ Description: "Feine brennbare Staube koennen in Verbindung mit einer Zuendquelle explosionsartig reagieren und schwere Zerstoerungen verursachen.",
+ DefaultSeverity: 5,
+ DefaultProbability: 2,
+ DefaultExposure: 3,
+ DefaultAvoidance: 2,
+ ApplicableComponentTypes: []string{"mechanical", "other"},
+ RegulationReferences: []string{"Maschinenverordnung 2023/1230 Anhang I"},
+ SuggestedMitigations: mustMarshalJSON([]string{"ATEX-konforme Ausfuehrung", "Absauganlage mit Funkenerkennung"}),
+ TypicalCauses: []string{"Unzureichende Absaugung", "Staubansammlung in Hohlraeumen", "Elektrische oder mechanische Zuendquelle"},
+ TypicalHarm: "Explosion mit toedlichen Verletzungen, schwere Verbrennungen, Gebaeudezerstoerung",
+ RelevantLifecyclePhases: []string{"normal_operation", "cleaning"},
+ RecommendedMeasuresDesign: []string{"ATEX-konforme Konstruktion", "Druckentlastungsflaechen vorsehen"},
+ RecommendedMeasuresTechnical: []string{"Absaugung mit Funkenloeschanlage", "Explosionsunterdrueckung"},
+ RecommendedMeasuresInformation: []string{"Ex-Zoneneinteilung kennzeichnen", "Reinigungsplan fuer Staubablagerungen"},
+ SuggestedEvidence: []string{"Explosionsschutz-Dokument", "ATEX-Konformitaetserklaerung"},
+ RelatedKeywords: []string{"Staubexplosion", "ATEX", "Feinstaub"},
+ IsBuiltin: true,
+ TenantID: nil,
+ CreatedAt: now,
+ },
+ {
+ ID: hazardUUID("material_environmental", 2),
+ Category: "material_environmental",
+ SubCategory: "rauch_gas",
+ Name: "Rauch- und Gasfreisetzung bei Laserschneiden",
+ Description: "Beim Laserschneiden entstehen gesundheitsschaedliche Rauche und Gase aus dem verdampften Material, die die Atemwege schaedigen.",
+ DefaultSeverity: 4,
+ DefaultProbability: 4,
+ DefaultExposure: 3,
+ DefaultAvoidance: 3,
+ ApplicableComponentTypes: []string{"mechanical", "other"},
+ RegulationReferences: []string{"Maschinenverordnung 2023/1230 Anhang I"},
+ SuggestedMitigations: mustMarshalJSON([]string{"Absaugung direkt am Bearbeitungspunkt", "Filteranlage mit Aktivkohlestufe"}),
+ TypicalCauses: []string{"Fehlende Absaugung", "Verarbeitung kunststoffbeschichteter Materialien", "Undichte Maschineneinhausung"},
+ TypicalHarm: "Atemwegserkrankungen, Reizung von Augen und Schleimhaeuten",
+ RelevantLifecyclePhases: []string{"normal_operation"},
+ RecommendedMeasuresDesign: []string{"Geschlossener Bearbeitungsraum mit Absaugung", "Materialauswahl ohne toxische Beschichtungen"},
+ RecommendedMeasuresTechnical: []string{"Punktabsaugung mit HEPA-Filter", "Raumluft-Monitoring"},
+ RecommendedMeasuresInformation: []string{"Materialfreigabeliste pflegen", "Atemschutz-PSA bei Sondermaterialien"},
+ SuggestedEvidence: []string{"Arbeitsplatzmessung Gefahrstoffe", "Risikobeurteilung"},
+ RelatedKeywords: []string{"Laserschneiden", "Rauch", "Gefahrstoff"},
+ IsBuiltin: true,
+ TenantID: nil,
+ CreatedAt: now,
+ },
+ {
+ ID: hazardUUID("material_environmental", 3),
+ Category: "material_environmental",
+ SubCategory: "kuehlschmierstoff",
+ Name: "Daempfe aus Kuehlschmierstoffen",
+ Description: "Beim Einsatz von Kuehlschmierstoffen entstehen Aerosole und Daempfe, die bei Langzeitexposition zu Haut- und Atemwegserkrankungen fuehren.",
+ DefaultSeverity: 3,
+ DefaultProbability: 3,
+ DefaultExposure: 4,
+ DefaultAvoidance: 3,
+ ApplicableComponentTypes: []string{"mechanical", "other"},
+ RegulationReferences: []string{"Maschinenverordnung 2023/1230 Anhang I"},
+ SuggestedMitigations: mustMarshalJSON([]string{"Oelunebelabscheider an der Maschine", "Hautschutzplan fuer Bediener"}),
+ TypicalCauses: []string{"Offene Bearbeitungszonen ohne Abschirmung", "Ueberaltertes Kuehlschmiermittel", "Zu hoher Kuehlmitteldurchsatz"},
+ TypicalHarm: "Oelakne, Atemwegssensibilisierung, allergische Hautreaktionen",
+ RelevantLifecyclePhases: []string{"normal_operation"},
+ RecommendedMeasuresDesign: []string{"Geschlossener Bearbeitungsraum", "Minimalmengenschmierung statt Volumenoelstrom"},
+ RecommendedMeasuresTechnical: []string{"Oelunebelabscheider", "KSS-Konzentrations- und pH-Ueberwachung"},
+ RecommendedMeasuresInformation: []string{"Hautschutz- und Hygieneplan", "KSS-Pflegeanweisung"},
+ SuggestedEvidence: []string{"Arbeitsplatz-Gefahrstoffmessung", "Hautschutzplan"},
+ RelatedKeywords: []string{"Kuehlschmierstoff", "KSS", "Aerosol"},
+ IsBuiltin: true,
+ TenantID: nil,
+ CreatedAt: now,
+ },
+ {
+ ID: hazardUUID("material_environmental", 4),
+ Category: "material_environmental",
+ SubCategory: "prozessmedien",
+ Name: "Chemische Exposition durch Prozessmedien",
+ Description: "Chemisch aggressive Prozessmedien wie Saeuren, Laugen oder Loesemittel koennen bei Hautkontakt oder Einatmen schwere Gesundheitsschaeden verursachen.",
+ DefaultSeverity: 4,
+ DefaultProbability: 3,
+ DefaultExposure: 3,
+ DefaultAvoidance: 3,
+ ApplicableComponentTypes: []string{"other", "mechanical"},
+ RegulationReferences: []string{"Maschinenverordnung 2023/1230 Anhang I"},
+ SuggestedMitigations: mustMarshalJSON([]string{"Geschlossene Mediumfuehrung", "Chemikalienschutz-PSA"}),
+ TypicalCauses: []string{"Offene Behaelter mit Gefahrstoffen", "Spritzer beim Nachfuellen", "Leckage an Dichtungen"},
+ TypicalHarm: "Veraetzungen, Atemwegsschaedigung, Vergiftung",
+ RelevantLifecyclePhases: []string{"normal_operation", "maintenance", "cleaning"},
+ RecommendedMeasuresDesign: []string{"Geschlossene Kreislaeufe", "Korrosionsbestaendige Materialien"},
+ RecommendedMeasuresTechnical: []string{"Notdusche und Augenspueler", "Gaswarnanlage"},
+ RecommendedMeasuresInformation: []string{"Sicherheitsdatenblaetter am Arbeitsplatz", "Gefahrstoffunterweisung"},
+ SuggestedEvidence: []string{"Gefahrstoffverzeichnis", "Arbeitsplatzmessung"},
+ RelatedKeywords: []string{"Gefahrstoff", "Chemikalie", "Prozessmedium"},
+ IsBuiltin: true,
+ TenantID: nil,
+ CreatedAt: now,
+ },
+ {
+ ID: hazardUUID("material_environmental", 5),
+ Category: "material_environmental",
+ SubCategory: "dichtungsversagen",
+ Name: "Leckage von Gefahrstoffen bei Dichtungsversagen",
+ Description: "Versagende Dichtungen an Rohrleitungen oder Behaeltern setzen Gefahrstoffe frei und gefaehrden Personal und Umwelt.",
+ DefaultSeverity: 4,
+ DefaultProbability: 3,
+ DefaultExposure: 3,
+ DefaultAvoidance: 3,
+ ApplicableComponentTypes: []string{"mechanical", "other"},
+ RegulationReferences: []string{"Maschinenverordnung 2023/1230 Anhang I"},
+ SuggestedMitigations: mustMarshalJSON([]string{"Doppelwandige Behaelter mit Leckagedetektion", "Auffangwannen unter allen Gefahrstoffbereichen"}),
+ TypicalCauses: []string{"Alterung der Dichtungsmaterialien", "Chemische Unvertraeglichkeit", "Thermische Ueberbelastung der Dichtung"},
+ TypicalHarm: "Gefahrstofffreisetzung mit Vergiftung, Hautschaedigung, Umweltschaden",
+ RelevantLifecyclePhases: []string{"normal_operation", "maintenance"},
+ RecommendedMeasuresDesign: []string{"Doppelwandige Leitungen", "Chemisch bestaendige Dichtungswerkstoffe"},
+ RecommendedMeasuresTechnical: []string{"Leckagesensoren", "Auffangwannen mit Fassungsvermoegen 110%"},
+ RecommendedMeasuresInformation: []string{"Dichtungswechselintervalle festlegen", "Notfallplan Gefahrstoffaustritt"},
+ SuggestedEvidence: []string{"Dichtheitspruefprotokoll", "Gefahrstoff-Notfallplan"},
+ RelatedKeywords: []string{"Leckage", "Dichtung", "Gefahrstoff"},
+ IsBuiltin: true,
+ TenantID: nil,
+ CreatedAt: now,
+ },
+ {
+ ID: hazardUUID("material_environmental", 6),
+ Category: "material_environmental",
+ SubCategory: "biologische_kontamination",
+ Name: "Biologische Kontamination in Lebensmittelmaschinen",
+ Description: "In Maschinen der Lebensmittelindustrie koennen sich Mikroorganismen in schwer zu reinigenden Bereichen ansiedeln und das Produkt kontaminieren.",
+ DefaultSeverity: 4,
+ DefaultProbability: 3,
+ DefaultExposure: 3,
+ DefaultAvoidance: 3,
+ ApplicableComponentTypes: []string{"mechanical", "other"},
+ RegulationReferences: []string{"Maschinenverordnung 2023/1230 Anhang I"},
+ SuggestedMitigations: mustMarshalJSON([]string{"Hygienic-Design-Konstruktion", "CIP-Reinigungssystem"}),
+ TypicalCauses: []string{"Totraeume und Hinterschneidungen in der Konstruktion", "Unzureichende Reinigung", "Poroese Oberflaechen"},
+ TypicalHarm: "Lebensmittelkontamination mit Gesundheitsgefaehrdung der Verbraucher",
+ RelevantLifecyclePhases: []string{"normal_operation", "cleaning", "maintenance"},
+ RecommendedMeasuresDesign: []string{"Hygienic Design nach EHEDG-Richtlinien", "Selbstentleerende Konstruktion"},
+ RecommendedMeasuresTechnical: []string{"CIP-Reinigungsanlage", "Oberflaechenguete Ra kleiner 0.8 Mikrometer"},
+ RecommendedMeasuresInformation: []string{"Reinigungs- und Desinfektionsplan", "Hygieneschulung des Personals"},
+ SuggestedEvidence: []string{"EHEDG-Zertifikat", "Mikrobiologische Abklatschproben"},
+ RelatedKeywords: []string{"Hygiene", "Kontamination", "Lebensmittelsicherheit"},
+ IsBuiltin: true,
+ TenantID: nil,
+ CreatedAt: now,
+ },
+ {
+ ID: hazardUUID("material_environmental", 7),
+ Category: "material_environmental",
+ SubCategory: "elektrostatik",
+ Name: "Statische Aufladung in staubhaltiger Umgebung",
+ Description: "In staubhaltiger oder explosionsfaehiger Atmosphaere kann eine elektrostatische Entladung als Zuendquelle wirken und eine Explosion oder einen Brand ausloesen.",
+ DefaultSeverity: 5,
+ DefaultProbability: 2,
+ DefaultExposure: 3,
+ DefaultAvoidance: 3,
+ ApplicableComponentTypes: []string{"mechanical", "electrical", "other"},
+ RegulationReferences: []string{"Maschinenverordnung 2023/1230 Anhang I"},
+ SuggestedMitigations: mustMarshalJSON([]string{"Erdung aller leitfaehigen Teile", "Ableitfaehige Bodenbeschichtung"}),
+ TypicalCauses: []string{"Nicht geerdete Maschinenteile", "Reibung von Kunststoffbaendern", "Niedrige Luftfeuchtigkeit"},
+ TypicalHarm: "Zuendung explosionsfaehiger Atmosphaere, Explosion oder Brand",
+ RelevantLifecyclePhases: []string{"normal_operation"},
+ RecommendedMeasuresDesign: []string{"Leitfaehige Materialien verwenden", "Erdungskonzept fuer alle Komponenten"},
+ RecommendedMeasuresTechnical: []string{"Ionisatoren zur Ladungsneutralisation", "Erdungsueberwachung"},
+ RecommendedMeasuresInformation: []string{"ESD-Hinweisschilder", "Schulung zur elektrostatischen Gefaehrdung"},
+ SuggestedEvidence: []string{"Erdungsmessung", "Explosionsschutz-Dokument"},
+ RelatedKeywords: []string{"Elektrostatik", "ESD", "Zuendquelle"},
+ IsBuiltin: true,
+ TenantID: nil,
+ CreatedAt: now,
+ },
+ {
+ ID: hazardUUID("material_environmental", 8),
+ Category: "material_environmental",
+ SubCategory: "uv_strahlung",
+ Name: "UV-Strahlung bei Schweiss- oder Haerteprozessen",
+ Description: "Schweissvorgaenge und UV-Haerteprozesse emittieren ultraviolette Strahlung, die Augen und Haut schwer schaedigen kann.",
+ DefaultSeverity: 4,
+ DefaultProbability: 3,
+ DefaultExposure: 3,
+ DefaultAvoidance: 3,
+ ApplicableComponentTypes: []string{"mechanical", "electrical", "other"},
+ RegulationReferences: []string{"Maschinenverordnung 2023/1230 Anhang I"},
+ SuggestedMitigations: mustMarshalJSON([]string{"Abschirmung des UV-Bereichs", "Schweissschutzschirm und Schutzkleidung"}),
+ TypicalCauses: []string{"Fehlende Abschirmung der UV-Quelle", "Reflexion an glaenzenden Oberflaechen", "Aufenthalt im Strahlungsbereich ohne Schutz"},
+ TypicalHarm: "Verblitzen der Augen, Hautverbrennungen, erhoehtes Hautkrebsrisiko bei Langzeitexposition",
+ RelevantLifecyclePhases: []string{"normal_operation", "maintenance"},
+ RecommendedMeasuresDesign: []string{"Vollstaendige Einhausung der UV-Quelle", "UV-absorbierende Schutzscheiben"},
+ RecommendedMeasuresTechnical: []string{"Schweissvorhaenge um den Arbeitsbereich", "UV-Sensor mit Abschaltung"},
+ RecommendedMeasuresInformation: []string{"Warnhinweis UV-Strahlung", "PSA-Pflicht: Schweissschutzhelm und Schutzkleidung"},
+ SuggestedEvidence: []string{"UV-Strahlungsmessung", "Risikobeurteilung"},
+ RelatedKeywords: []string{"UV-Strahlung", "Schweissen", "Strahlenschutz"},
+ IsBuiltin: true,
+ TenantID: nil,
+ CreatedAt: now,
+ },
+ }
+}
diff --git a/ai-compliance-sdk/internal/iace/hazard_library_iso12100_mechanical.go b/ai-compliance-sdk/internal/iace/hazard_library_iso12100_mechanical.go
new file mode 100644
index 0000000..5385ec0
--- /dev/null
+++ b/ai-compliance-sdk/internal/iace/hazard_library_iso12100_mechanical.go
@@ -0,0 +1,362 @@
+package iace
+
+import "time"
+
+// builtinHazardsISO12100Mechanical returns ISO 12100 mechanical hazard
+// entries (indices 7-20) per Maschinenverordnung 2023/1230 and ISO 12100.
+func builtinHazardsISO12100Mechanical() []HazardLibraryEntry {
+ now := time.Now()
+ return []HazardLibraryEntry{
+ // ====================================================================
+ {
+ ID: hazardUUID("mechanical_hazard", 7),
+ Category: "mechanical_hazard",
+ SubCategory: "quetschgefahr",
+ Name: "Quetschgefahr durch gegenlaeufige Walzen",
+ Description: "Zwischen gegenlaeufig rotierenden Walzen entsteht ein Einzugspunkt, an dem Koerperteile oder Kleidung eingezogen und gequetscht werden koennen.",
+ DefaultSeverity: 5,
+ DefaultProbability: 3,
+ DefaultExposure: 3,
+ DefaultAvoidance: 3,
+ ApplicableComponentTypes: []string{"mechanical", "actuator"},
+ RegulationReferences: []string{"Maschinenverordnung 2023/1230 Anhang I"},
+ SuggestedMitigations: mustMarshalJSON([]string{"Feststehende trennende Schutzeinrichtung am Walzeneinlauf", "Zweihandbedienung bei manueller Beschickung"}),
+ TypicalCauses: []string{"Fehlende Schutzabdeckung am Einzugspunkt", "Manuelle Materialzufuehrung ohne Hilfsmittel", "Wartung bei laufender Maschine"},
+ TypicalHarm: "Quetschverletzungen an Fingern, Haenden oder Armen bis hin zu Amputationen",
+ RelevantLifecyclePhases: []string{"normal_operation", "maintenance", "setup"},
+ RecommendedMeasuresDesign: []string{"Mindestabstand zwischen Walzen groesser als 25 mm oder kleiner als 5 mm", "Einzugspunkt ausserhalb der Reichweite positionieren"},
+ RecommendedMeasuresTechnical: []string{"Schutzgitter mit Sicherheitsverriegelung", "Lichtschranke vor dem Einzugsbereich"},
+ RecommendedMeasuresInformation: []string{"Warnschilder am Einzugspunkt", "Betriebsanweisung zur sicheren Beschickung"},
+ SuggestedEvidence: []string{"Pruefbericht der Schutzeinrichtung", "Risikobeurteilung nach ISO 12100"},
+ RelatedKeywords: []string{"Walzen", "Einzugspunkt", "Quetschstelle"},
+ IsBuiltin: true,
+ TenantID: nil,
+ CreatedAt: now,
+ },
+ {
+ ID: hazardUUID("mechanical_hazard", 8),
+ Category: "mechanical_hazard",
+ SubCategory: "schergefahr",
+ Name: "Schergefahr an beweglichen Maschinenteilen",
+ Description: "Durch gegeneinander bewegte Maschinenteile entstehen Scherstellen, die zu schweren Schnitt- und Trennverletzungen fuehren koennen.",
+ DefaultSeverity: 5,
+ DefaultProbability: 3,
+ DefaultExposure: 3,
+ DefaultAvoidance: 3,
+ ApplicableComponentTypes: []string{"mechanical", "actuator"},
+ RegulationReferences: []string{"Maschinenverordnung 2023/1230 Anhang I"},
+ SuggestedMitigations: mustMarshalJSON([]string{"Trennende Schutzeinrichtung an der Scherstelle", "Sicherheitsabstand nach ISO 13857"}),
+ TypicalCauses: []string{"Unzureichender Sicherheitsabstand", "Fehlende Schutzverkleidung", "Eingriff waehrend des Betriebs"},
+ TypicalHarm: "Schnitt- und Trennverletzungen an Fingern und Haenden",
+ RelevantLifecyclePhases: []string{"normal_operation", "maintenance"},
+ RecommendedMeasuresDesign: []string{"Sicherheitsabstaende nach ISO 13857 einhalten", "Scherstellen konstruktiv vermeiden"},
+ RecommendedMeasuresTechnical: []string{"Verriegelte Schutzhauben", "Not-Halt in unmittelbarer Naehe"},
+ RecommendedMeasuresInformation: []string{"Gefahrenhinweis an Scherstellen", "Schulung der Bediener"},
+ SuggestedEvidence: []string{"Abstandsmessung gemaess ISO 13857", "Risikobeurteilung"},
+ RelatedKeywords: []string{"Scherstelle", "Gegenlaeufig", "Schneidgefahr"},
+ IsBuiltin: true,
+ TenantID: nil,
+ CreatedAt: now,
+ },
+ {
+ ID: hazardUUID("mechanical_hazard", 9),
+ Category: "mechanical_hazard",
+ SubCategory: "schneidgefahr",
+ Name: "Schneidgefahr durch rotierende Werkzeuge",
+ Description: "Rotierende Schneidwerkzeuge wie Fraeser, Saegeblaetter oder Messer koennen bei Kontakt schwere Schnittverletzungen verursachen.",
+ DefaultSeverity: 5,
+ DefaultProbability: 3,
+ DefaultExposure: 3,
+ DefaultAvoidance: 2,
+ ApplicableComponentTypes: []string{"mechanical"},
+ RegulationReferences: []string{"Maschinenverordnung 2023/1230 Anhang I"},
+ SuggestedMitigations: mustMarshalJSON([]string{"Vollstaendige Einhausung des Werkzeugs", "Automatische Werkzeugbremse bei Schutztueroeffnung"}),
+ TypicalCauses: []string{"Offene Schutzhaube waehrend des Betriebs", "Nachlauf des Werkzeugs nach Abschaltung", "Werkzeugbruch"},
+ TypicalHarm: "Tiefe Schnittwunden bis hin zu Gliedmassentrennung",
+ RelevantLifecyclePhases: []string{"normal_operation", "setup", "maintenance"},
+ RecommendedMeasuresDesign: []string{"Vollstaendige Einhausung mit Verriegelung", "Werkzeugbremse mit kurzer Nachlaufzeit"},
+ RecommendedMeasuresTechnical: []string{"Verriegelte Schutzhaube mit Zuhaltung", "Drehzahlueberwachung"},
+ RecommendedMeasuresInformation: []string{"Warnhinweis zur Nachlaufzeit", "Betriebsanweisung zum Werkzeugwechsel"},
+ SuggestedEvidence: []string{"Nachlaufzeitmessung", "Pruefbericht Schutzeinrichtung"},
+ RelatedKeywords: []string{"Fraeser", "Saegeblatt", "Schneidwerkzeug"},
+ IsBuiltin: true,
+ TenantID: nil,
+ CreatedAt: now,
+ },
+ {
+ ID: hazardUUID("mechanical_hazard", 10),
+ Category: "mechanical_hazard",
+ SubCategory: "einzugsgefahr",
+ Name: "Einzugsgefahr durch Foerderbaender",
+ Description: "An Umlenkrollen und Antriebstrommeln von Foerderbaendern bestehen Einzugsstellen, die Koerperteile oder Kleidung erfassen koennen.",
+ DefaultSeverity: 4,
+ DefaultProbability: 3,
+ DefaultExposure: 4,
+ DefaultAvoidance: 3,
+ ApplicableComponentTypes: []string{"mechanical", "actuator"},
+ RegulationReferences: []string{"Maschinenverordnung 2023/1230 Anhang I"},
+ SuggestedMitigations: mustMarshalJSON([]string{"Schutzverkleidung an Umlenkrollen", "Not-Halt-Reissleine entlang des Foerderbands"}),
+ TypicalCauses: []string{"Fehlende Abdeckung an Umlenkpunkten", "Reinigung bei laufendem Band", "Lose Kleidung des Personals"},
+ TypicalHarm: "Einzugsverletzungen an Armen und Haenden, Quetschungen",
+ RelevantLifecyclePhases: []string{"normal_operation", "cleaning", "maintenance"},
+ RecommendedMeasuresDesign: []string{"Umlenkrollen mit Schutzverkleidung", "Unterflur-Foerderung wo moeglich"},
+ RecommendedMeasuresTechnical: []string{"Not-Halt-Reissleine", "Bandschieflauf-Erkennung"},
+ RecommendedMeasuresInformation: []string{"Kleidervorschrift fuer Bedienpersonal", "Sicherheitsunterweisung"},
+ SuggestedEvidence: []string{"Pruefbericht der Schutzeinrichtungen", "Risikobeurteilung"},
+ RelatedKeywords: []string{"Foerderband", "Umlenkrolle", "Einzugsstelle"},
+ IsBuiltin: true,
+ TenantID: nil,
+ CreatedAt: now,
+ },
+ {
+ ID: hazardUUID("mechanical_hazard", 11),
+ Category: "mechanical_hazard",
+ SubCategory: "erfassungsgefahr",
+ Name: "Erfassungsgefahr durch rotierende Wellen",
+ Description: "Freiliegende rotierende Wellen, Kupplungen oder Zapfen koennen Kleidung oder Haare erfassen und Personen in die Drehbewegung hineinziehen.",
+ DefaultSeverity: 5,
+ DefaultProbability: 3,
+ DefaultExposure: 3,
+ DefaultAvoidance: 2,
+ ApplicableComponentTypes: []string{"mechanical", "actuator"},
+ RegulationReferences: []string{"Maschinenverordnung 2023/1230 Anhang I"},
+ SuggestedMitigations: mustMarshalJSON([]string{"Vollstaendige Verkleidung rotierender Wellen", "Drehmomentbegrenzung"}),
+ TypicalCauses: []string{"Fehlende Wellenabdeckung", "Lose Kleidungsstuecke", "Wartung bei laufender Welle"},
+ TypicalHarm: "Erfassungsverletzungen mit Knochenbruechen, Skalpierungen oder toedlichem Ausgang",
+ RelevantLifecyclePhases: []string{"normal_operation", "maintenance"},
+ RecommendedMeasuresDesign: []string{"Wellen vollstaendig einhausen", "Kupplungen mit Schutzhuelsen"},
+ RecommendedMeasuresTechnical: []string{"Verriegelte Schutzabdeckung", "Stillstandsueberwachung fuer Wartungszugang"},
+ RecommendedMeasuresInformation: []string{"Kleiderordnung ohne lose Teile", "Warnschilder an Wellenabdeckungen"},
+ SuggestedEvidence: []string{"Inspektionsbericht Wellenabdeckungen", "Risikobeurteilung"},
+ RelatedKeywords: []string{"Welle", "Kupplung", "Erfassung"},
+ IsBuiltin: true,
+ TenantID: nil,
+ CreatedAt: now,
+ },
+ {
+ ID: hazardUUID("mechanical_hazard", 12),
+ Category: "mechanical_hazard",
+ SubCategory: "stossgefahr",
+ Name: "Stossgefahr durch pneumatische/hydraulische Zylinder",
+ Description: "Schnell ausfahrende Pneumatik- oder Hydraulikzylinder koennen Personen stossen oder einklemmen, insbesondere bei unerwartetem Anlauf.",
+ DefaultSeverity: 4,
+ DefaultProbability: 3,
+ DefaultExposure: 3,
+ DefaultAvoidance: 3,
+ ApplicableComponentTypes: []string{"actuator", "mechanical"},
+ RegulationReferences: []string{"Maschinenverordnung 2023/1230 Anhang I"},
+ SuggestedMitigations: mustMarshalJSON([]string{"Geschwindigkeitsbegrenzung durch Drosselventile", "Schutzeinrichtung im Bewegungsbereich"}),
+ TypicalCauses: []string{"Fehlende Endlagendaempfung", "Unerwarteter Druckaufbau", "Aufenthalt im Bewegungsbereich"},
+ TypicalHarm: "Prellungen, Knochenbrueche, Einklemmverletzungen",
+ RelevantLifecyclePhases: []string{"normal_operation", "setup", "maintenance"},
+ RecommendedMeasuresDesign: []string{"Endlagendaempfung vorsehen", "Zylindergeschwindigkeit begrenzen"},
+ RecommendedMeasuresTechnical: []string{"Lichtvorhang im Bewegungsbereich", "Druckspeicher-Entlastungsventil"},
+ RecommendedMeasuresInformation: []string{"Kennzeichnung des Bewegungsbereichs", "Betriebsanweisung"},
+ SuggestedEvidence: []string{"Geschwindigkeitsmessung", "Risikobeurteilung"},
+ RelatedKeywords: []string{"Zylinder", "Pneumatik", "Stossgefahr"},
+ IsBuiltin: true,
+ TenantID: nil,
+ CreatedAt: now,
+ },
+ {
+ ID: hazardUUID("mechanical_hazard", 13),
+ Category: "mechanical_hazard",
+ SubCategory: "herabfallende_teile",
+ Name: "Herabfallende Teile aus Werkstueckhalterung",
+ Description: "Unzureichend gesicherte Werkstuecke oder Werkzeuge koennen sich aus der Halterung loesen und herabfallen.",
+ DefaultSeverity: 4,
+ DefaultProbability: 2,
+ DefaultExposure: 3,
+ DefaultAvoidance: 3,
+ ApplicableComponentTypes: []string{"mechanical"},
+ RegulationReferences: []string{"Maschinenverordnung 2023/1230 Anhang I"},
+ SuggestedMitigations: mustMarshalJSON([]string{"Spannkraftueberwachung der Halterung", "Schutzdach ueber dem Bedienerbereich"}),
+ TypicalCauses: []string{"Unzureichende Spannkraft", "Vibration lockert die Halterung", "Falsches Werkstueck-Spannmittel"},
+ TypicalHarm: "Kopfverletzungen, Prellungen, Quetschungen durch herabfallende Teile",
+ RelevantLifecyclePhases: []string{"normal_operation", "setup"},
+ RecommendedMeasuresDesign: []string{"Spannkraftueberwachung mit Abschaltung", "Auffangvorrichtung unter Werkstueck"},
+ RecommendedMeasuresTechnical: []string{"Sensor zur Spannkraftueberwachung", "Schutzhaube"},
+ RecommendedMeasuresInformation: []string{"Pruefanweisung vor Bearbeitungsstart", "Schutzhelmpflicht im Gefahrenbereich"},
+ SuggestedEvidence: []string{"Pruefprotokoll Spannmittel", "Risikobeurteilung"},
+ RelatedKeywords: []string{"Werkstueck", "Spannmittel", "Herabfallen"},
+ IsBuiltin: true,
+ TenantID: nil,
+ CreatedAt: now,
+ },
+ {
+ ID: hazardUUID("mechanical_hazard", 14),
+ Category: "mechanical_hazard",
+ SubCategory: "wegschleudern",
+ Name: "Wegschleudern von Bruchstuecken bei Werkzeugversagen",
+ Description: "Bei Werkzeugbruch koennen Bruchstuecke mit hoher Geschwindigkeit weggeschleudert werden und Personen im Umfeld verletzen.",
+ DefaultSeverity: 5,
+ DefaultProbability: 2,
+ DefaultExposure: 3,
+ DefaultAvoidance: 2,
+ ApplicableComponentTypes: []string{"mechanical"},
+ RegulationReferences: []string{"Maschinenverordnung 2023/1230 Anhang I"},
+ SuggestedMitigations: mustMarshalJSON([]string{"Splitterschutzscheibe aus Polycarbonat", "Regelmaessige Werkzeuginspektion"}),
+ TypicalCauses: []string{"Werkzeugverschleiss", "Ueberschreitung der zulaessigen Drehzahl", "Materialfehler im Werkzeug"},
+ TypicalHarm: "Durchdringende Verletzungen durch Bruchstuecke, Augenverletzungen",
+ RelevantLifecyclePhases: []string{"normal_operation"},
+ RecommendedMeasuresDesign: []string{"Splitterschutz in der Einhausung", "Drehzahlbegrenzung des Werkzeugs"},
+ RecommendedMeasuresTechnical: []string{"Unwuchtueberwachung", "Brucherkennungssensor"},
+ RecommendedMeasuresInformation: []string{"Maximaldrehzahl am Werkzeug kennzeichnen", "Schutzbrillenpflicht"},
+ SuggestedEvidence: []string{"Bersttest der Einhausung", "Werkzeuginspektionsprotokoll"},
+ RelatedKeywords: []string{"Werkzeugbruch", "Splitter", "Schleudern"},
+ IsBuiltin: true,
+ TenantID: nil,
+ CreatedAt: now,
+ },
+ {
+ ID: hazardUUID("mechanical_hazard", 15),
+ Category: "mechanical_hazard",
+ SubCategory: "instabilitaet",
+ Name: "Instabilitaet der Maschine durch fehlendes Fundament",
+ Description: "Eine unzureichend verankerte oder falsch aufgestellte Maschine kann kippen oder sich verschieben, insbesondere bei dynamischen Kraeften.",
+ DefaultSeverity: 4,
+ DefaultProbability: 2,
+ DefaultExposure: 2,
+ DefaultAvoidance: 3,
+ ApplicableComponentTypes: []string{"mechanical"},
+ RegulationReferences: []string{"Maschinenverordnung 2023/1230 Anhang I"},
+ SuggestedMitigations: mustMarshalJSON([]string{"Fundamentberechnung und Verankerung", "Standsicherheitsnachweis"}),
+ TypicalCauses: []string{"Fehlende Bodenverankerung", "Ungeeigneter Untergrund", "Erhoehte dynamische Lasten"},
+ TypicalHarm: "Quetschverletzungen durch kippende Maschine, Sachschaeden",
+ RelevantLifecyclePhases: []string{"installation", "normal_operation", "transport"},
+ RecommendedMeasuresDesign: []string{"Niedriger Schwerpunkt der Maschine", "Befestigungspunkte im Maschinenrahmen"},
+ RecommendedMeasuresTechnical: []string{"Bodenverankerung mit Schwerlastduebeln", "Nivellierelemente mit Kippsicherung"},
+ RecommendedMeasuresInformation: []string{"Aufstellanleitung mit Fundamentplan", "Hinweis auf maximale Bodenbelastung"},
+ SuggestedEvidence: []string{"Standsicherheitsnachweis", "Fundamentplan"},
+ RelatedKeywords: []string{"Fundament", "Standsicherheit", "Kippen"},
+ IsBuiltin: true,
+ TenantID: nil,
+ CreatedAt: now,
+ },
+ {
+ ID: hazardUUID("mechanical_hazard", 16),
+ Category: "mechanical_hazard",
+ SubCategory: "wiederanlauf",
+ Name: "Unkontrollierter Wiederanlauf nach Energieunterbruch",
+ Description: "Nach einem Stromausfall oder Druckabfall kann die Maschine unkontrolliert wieder anlaufen und Personen im Gefahrenbereich verletzen.",
+ DefaultSeverity: 5,
+ DefaultProbability: 3,
+ DefaultExposure: 3,
+ DefaultAvoidance: 2,
+ ApplicableComponentTypes: []string{"mechanical", "controller", "electrical"},
+ RegulationReferences: []string{"Maschinenverordnung 2023/1230 Anhang I"},
+ SuggestedMitigations: mustMarshalJSON([]string{"Wiederanlaufsperre nach Energierueckkehr", "Quittierungspflichtiger Neustart"}),
+ TypicalCauses: []string{"Fehlende Wiederanlaufsperre", "Stromausfall mit anschliessendem automatischem Neustart", "Druckaufbau nach Leckagereparatur"},
+ TypicalHarm: "Verletzungen durch unerwartete Maschinenbewegung bei Wiederanlauf",
+ RelevantLifecyclePhases: []string{"normal_operation", "maintenance", "fault_finding"},
+ RecommendedMeasuresDesign: []string{"Wiederanlaufsperre in der Steuerung", "Energiespeicher sicher entladen"},
+ RecommendedMeasuresTechnical: []string{"Schaltschuetz mit Selbsthaltung", "Druckschalter mit Ruecksetzbedingung"},
+ RecommendedMeasuresInformation: []string{"Hinweis auf Wiederanlaufverhalten", "Verfahrensanweisung nach Energieausfall"},
+ SuggestedEvidence: []string{"Funktionstest Wiederanlaufsperre", "Risikobeurteilung"},
+ RelatedKeywords: []string{"Wiederanlauf", "Stromausfall", "Anlaufsperre"},
+ IsBuiltin: true,
+ TenantID: nil,
+ CreatedAt: now,
+ },
+ {
+ ID: hazardUUID("mechanical_hazard", 17),
+ Category: "mechanical_hazard",
+ SubCategory: "reibungsgefahr",
+ Name: "Reibungsgefahr an rauen Oberflaechen",
+ Description: "Raue, scharfkantige oder gratbehaftete Maschinenoberlaechen koennen bei Kontakt zu Hautabschuerfungen und Schnittverletzungen fuehren.",
+ DefaultSeverity: 3,
+ DefaultProbability: 3,
+ DefaultExposure: 4,
+ DefaultAvoidance: 4,
+ ApplicableComponentTypes: []string{"mechanical"},
+ RegulationReferences: []string{"Maschinenverordnung 2023/1230 Anhang I"},
+ SuggestedMitigations: mustMarshalJSON([]string{"Entgraten aller zugaenglichen Kanten", "Schutzhandschuhe fuer Bedienpersonal"}),
+ TypicalCauses: []string{"Nicht entgratete Schnittkanten", "Korrosionsraue Oberflaechen", "Verschleissbedingter Materialabtrag"},
+ TypicalHarm: "Hautabschuerfungen, Schnittverletzungen an Haenden und Armen",
+ RelevantLifecyclePhases: []string{"normal_operation", "maintenance", "setup"},
+ RecommendedMeasuresDesign: []string{"Kanten brechen oder abrunden", "Glatte Oberflaechen an Kontaktstellen"},
+ RecommendedMeasuresTechnical: []string{"Kantenschutzprofile anbringen"},
+ RecommendedMeasuresInformation: []string{"Hinweis auf scharfe Kanten", "Handschuhpflicht in der Betriebsanweisung"},
+ SuggestedEvidence: []string{"Oberflaechenpruefung", "Risikobeurteilung"},
+ RelatedKeywords: []string{"Grat", "Scharfkantig", "Oberflaeche"},
+ IsBuiltin: true,
+ TenantID: nil,
+ CreatedAt: now,
+ },
+ {
+ ID: hazardUUID("mechanical_hazard", 18),
+ Category: "mechanical_hazard",
+ SubCategory: "hochdruckstrahl",
+ Name: "Fluessigkeitshochdruckstrahl",
+ Description: "Hochdruckstrahlen aus Hydraulik-, Kuehl- oder Reinigungssystemen koennen Haut durchdringen und schwere Gewebeschaeden verursachen.",
+ DefaultSeverity: 5,
+ DefaultProbability: 2,
+ DefaultExposure: 2,
+ DefaultAvoidance: 2,
+ ApplicableComponentTypes: []string{"mechanical", "actuator"},
+ RegulationReferences: []string{"Maschinenverordnung 2023/1230 Anhang I"},
+ SuggestedMitigations: mustMarshalJSON([]string{"Abschirmung von Hochdruckleitungen", "Regelmaessige Leitungsinspektion"}),
+ TypicalCauses: []string{"Leitungsbruch unter Hochdruck", "Undichte Verschraubungen", "Alterung von Schlauchleitungen"},
+ TypicalHarm: "Hochdruckinjektionsverletzungen, Gewebsnekrose",
+ RelevantLifecyclePhases: []string{"normal_operation", "maintenance"},
+ RecommendedMeasuresDesign: []string{"Schlauchbruchsicherungen einbauen", "Leitungen ausserhalb des Aufenthaltsbereichs verlegen"},
+ RecommendedMeasuresTechnical: []string{"Druckabschaltung bei Leitungsbruch", "Schutzblechverkleidung"},
+ RecommendedMeasuresInformation: []string{"Warnhinweis an Hochdruckleitungen", "Prueffristen fuer Schlauchleitungen"},
+ SuggestedEvidence: []string{"Druckpruefprotokoll", "Inspektionsbericht Schlauchleitungen"},
+ RelatedKeywords: []string{"Hochdruck", "Hydraulikleitung", "Injection"},
+ IsBuiltin: true,
+ TenantID: nil,
+ CreatedAt: now,
+ },
+ {
+ ID: hazardUUID("mechanical_hazard", 19),
+ Category: "mechanical_hazard",
+ SubCategory: "federelemente",
+ Name: "Gefahr durch federgespannte Elemente",
+ Description: "Unter Spannung stehende Federn oder elastische Elemente koennen bei unkontrolliertem Loesen Teile wegschleudern oder Personen verletzen.",
+ DefaultSeverity: 4,
+ DefaultProbability: 2,
+ DefaultExposure: 2,
+ DefaultAvoidance: 3,
+ ApplicableComponentTypes: []string{"mechanical"},
+ RegulationReferences: []string{"Maschinenverordnung 2023/1230 Anhang I"},
+ SuggestedMitigations: mustMarshalJSON([]string{"Gesicherte Federentspannung vor Demontage", "Warnung bei vorgespannten Elementen"}),
+ TypicalCauses: []string{"Demontage ohne vorherige Entspannung", "Materialermuedung der Feder", "Fehlende Kennzeichnung vorgespannter Elemente"},
+ TypicalHarm: "Verletzungen durch wegschleudernde Federelemente, Prellungen",
+ RelevantLifecyclePhases: []string{"maintenance", "decommissioning"},
+ RecommendedMeasuresDesign: []string{"Sichere Entspannungsmoeglichkeit vorsehen", "Federn mit Bruchsicherung"},
+ RecommendedMeasuresTechnical: []string{"Spezialwerkzeug zur Federentspannung"},
+ RecommendedMeasuresInformation: []string{"Kennzeichnung vorgespannter Elemente", "Wartungsanweisung mit Entspannungsprozedur"},
+ SuggestedEvidence: []string{"Wartungsanweisung", "Risikobeurteilung"},
+ RelatedKeywords: []string{"Feder", "Vorspannung", "Energiespeicher"},
+ IsBuiltin: true,
+ TenantID: nil,
+ CreatedAt: now,
+ },
+ {
+ ID: hazardUUID("mechanical_hazard", 20),
+ Category: "mechanical_hazard",
+ SubCategory: "schutztor",
+ Name: "Quetschgefahr im Schliessbereich von Schutztoren",
+ Description: "Automatisch schliessende Schutztore und -tueren koennen Personen im Schliessbereich einklemmen oder quetschen.",
+ DefaultSeverity: 4,
+ DefaultProbability: 3,
+ DefaultExposure: 4,
+ DefaultAvoidance: 3,
+ ApplicableComponentTypes: []string{"mechanical", "actuator", "sensor"},
+ RegulationReferences: []string{"Maschinenverordnung 2023/1230 Anhang I"},
+ SuggestedMitigations: mustMarshalJSON([]string{"Schliesskantensicherung mit Kontaktleiste", "Lichtschranke im Schliessbereich"}),
+ TypicalCauses: []string{"Fehlende Schliesskantensicherung", "Defekter Sensor", "Person im Schliessbereich nicht erkannt"},
+ TypicalHarm: "Quetschverletzungen an Koerper oder Gliedmassen",
+ RelevantLifecyclePhases: []string{"normal_operation", "maintenance"},
+ RecommendedMeasuresDesign: []string{"Schliesskraftbegrenzung", "Reversierautomatik bei Hindernis"},
+ RecommendedMeasuresTechnical: []string{"Kontaktleiste an der Schliesskante", "Lichtschranke im Durchgangsbereich"},
+ RecommendedMeasuresInformation: []string{"Warnhinweis am Schutztor", "Automatik-Betrieb kennzeichnen"},
+ SuggestedEvidence: []string{"Schliesskraftmessung", "Funktionstest Reversierautomatik"},
+ RelatedKeywords: []string{"Schutztor", "Schliesskante", "Einklemmen"},
+ IsBuiltin: true,
+ TenantID: nil,
+ CreatedAt: now,
+ },
+ }
+}
diff --git a/ai-compliance-sdk/internal/iace/hazard_library_iso12100_pneumatic.go b/ai-compliance-sdk/internal/iace/hazard_library_iso12100_pneumatic.go
new file mode 100644
index 0000000..b3f023b
--- /dev/null
+++ b/ai-compliance-sdk/internal/iace/hazard_library_iso12100_pneumatic.go
@@ -0,0 +1,417 @@
+package iace
+
+import "time"
+
+// builtinHazardsISO12100Pneumatic returns ISO 12100 pneumatic/hydraulic
+// and noise/vibration hazard entries per Maschinenverordnung 2023/1230.
+func builtinHazardsISO12100Pneumatic() []HazardLibraryEntry {
+ now := time.Now()
+ return []HazardLibraryEntry{
+ // ====================================================================
+ // Category: pneumatic_hydraulic (indices 1-10, 10 entries)
+ // ====================================================================
+ {
+ ID: hazardUUID("pneumatic_hydraulic", 1),
+ Category: "pneumatic_hydraulic",
+ SubCategory: "druckverlust",
+ Name: "Unkontrollierter Druckverlust in pneumatischem System",
+ Description: "Ein ploetzlicher Druckabfall im Pneumatiksystem kann zum Versagen von Halte- und Klemmfunktionen fuehren, wodurch Werkstuecke herabfallen oder Achsen absacken.",
+ DefaultSeverity: 4,
+ DefaultProbability: 3,
+ DefaultExposure: 3,
+ DefaultAvoidance: 3,
+ ApplicableComponentTypes: []string{"actuator", "mechanical"},
+ RegulationReferences: []string{"Maschinenverordnung 2023/1230 Anhang I"},
+ SuggestedMitigations: mustMarshalJSON([]string{"Rueckschlagventile in Haltezylinderleitungen", "Druckueberwachung mit sicherer Abschaltung"}),
+ TypicalCauses: []string{"Kompressorausfall", "Leckage in der Versorgungsleitung", "Fehlerhaftes Druckregelventil"},
+ TypicalHarm: "Quetschverletzungen durch absackende Achsen oder herabfallende Werkstuecke",
+ RelevantLifecyclePhases: []string{"normal_operation", "fault_finding"},
+ RecommendedMeasuresDesign: []string{"Mechanische Haltebremsen als Rueckfallebene", "Rueckschlagventile in sicherheitsrelevanten Leitungen"},
+ RecommendedMeasuresTechnical: []string{"Druckwaechter mit sicherer Reaktion", "Druckspeicher fuer Notbetrieb"},
+ RecommendedMeasuresInformation: []string{"Warnung bei Druckabfall", "Verfahrensanweisung fuer Druckausfall"},
+ SuggestedEvidence: []string{"Druckabfalltest", "Risikobeurteilung"},
+ RelatedKeywords: []string{"Druckverlust", "Pneumatik", "Haltefunktion"},
+ IsBuiltin: true,
+ TenantID: nil,
+ CreatedAt: now,
+ },
+ {
+ ID: hazardUUID("pneumatic_hydraulic", 2),
+ Category: "pneumatic_hydraulic",
+ SubCategory: "druckfreisetzung",
+ Name: "Ploetzliche Druckfreisetzung bei Leitungsbruch",
+ Description: "Ein Bersten oder Abreissen einer Druckleitung setzt schlagartig Energie frei, wobei Medien und Leitungsbruchstuecke weggeschleudert werden.",
+ DefaultSeverity: 5,
+ DefaultProbability: 2,
+ DefaultExposure: 3,
+ DefaultAvoidance: 2,
+ ApplicableComponentTypes: []string{"mechanical", "actuator"},
+ RegulationReferences: []string{"Maschinenverordnung 2023/1230 Anhang I"},
+ SuggestedMitigations: mustMarshalJSON([]string{"Schlauchbruchsicherungen", "Druckfeste Leitungsverlegung"}),
+ TypicalCauses: []string{"Materialermuedung der Leitung", "Ueberdruckbetrieb", "Mechanische Beschaedigung der Leitung"},
+ TypicalHarm: "Verletzungen durch weggeschleuderte Leitungsteile und austretende Druckmedien",
+ RelevantLifecyclePhases: []string{"normal_operation", "maintenance"},
+ RecommendedMeasuresDesign: []string{"Berstdruckfest dimensionierte Leitungen", "Leitungen in Schutzrohren verlegen"},
+ RecommendedMeasuresTechnical: []string{"Durchflussbegrenzer nach Druckquelle", "Schlauchbruchventile"},
+ RecommendedMeasuresInformation: []string{"Prueffristen fuer Druckleitungen", "Warnhinweis an Hochdruckbereichen"},
+ SuggestedEvidence: []string{"Druckpruefprotokoll", "Inspektionsbericht Leitungen"},
+ RelatedKeywords: []string{"Leitungsbruch", "Druckfreisetzung", "Bersten"},
+ IsBuiltin: true,
+ TenantID: nil,
+ CreatedAt: now,
+ },
+ {
+ ID: hazardUUID("pneumatic_hydraulic", 3),
+ Category: "pneumatic_hydraulic",
+ SubCategory: "schlauchpeitschen",
+ Name: "Schlauchpeitschen durch Berstversagen",
+ Description: "Ein unter Druck stehender Schlauch kann bei Versagen unkontrolliert umherschlagen und Personen im Umfeld treffen.",
+ DefaultSeverity: 4,
+ DefaultProbability: 2,
+ DefaultExposure: 3,
+ DefaultAvoidance: 2,
+ ApplicableComponentTypes: []string{"mechanical"},
+ RegulationReferences: []string{"Maschinenverordnung 2023/1230 Anhang I"},
+ SuggestedMitigations: mustMarshalJSON([]string{"Fangseile an Schlauchleitungen", "Schlauchbruchventile"}),
+ TypicalCauses: []string{"Alterung des Schlauchmaterials", "Knicke in der Schlauchfuehrung", "Falsche Schlauchtype fuer das Medium"},
+ TypicalHarm: "Peitschenverletzungen, Prellungen, Augenverletzungen",
+ RelevantLifecyclePhases: []string{"normal_operation", "maintenance"},
+ RecommendedMeasuresDesign: []string{"Fangseile oder Ketten an allen Schlauchleitungen", "Festverrohrung statt Schlauch wo moeglich"},
+ RecommendedMeasuresTechnical: []string{"Schlauchbruchventil am Anschluss"},
+ RecommendedMeasuresInformation: []string{"Tauschintervalle fuer Schlauchleitungen", "Kennzeichnung mit Herstelldatum"},
+ SuggestedEvidence: []string{"Schlauchleitungspruefprotokoll", "Risikobeurteilung"},
+ RelatedKeywords: []string{"Schlauch", "Peitschen", "Fangseil"},
+ IsBuiltin: true,
+ TenantID: nil,
+ CreatedAt: now,
+ },
+ {
+ ID: hazardUUID("pneumatic_hydraulic", 4),
+ Category: "pneumatic_hydraulic",
+ SubCategory: "druckspeicherenergie",
+ Name: "Unerwartete Bewegung durch Druckspeicherrestenergie",
+ Description: "Nach dem Abschalten der Maschine kann in Druckspeichern verbliebene Energie unerwartete Bewegungen von Zylindern oder Aktoren verursachen.",
+ DefaultSeverity: 5,
+ DefaultProbability: 3,
+ DefaultExposure: 2,
+ DefaultAvoidance: 2,
+ ApplicableComponentTypes: []string{"actuator", "mechanical"},
+ RegulationReferences: []string{"Maschinenverordnung 2023/1230 Anhang I"},
+ SuggestedMitigations: mustMarshalJSON([]string{"Automatische Druckspeicher-Entladung bei Abschaltung", "Sperrventile vor Aktoren"}),
+ TypicalCauses: []string{"Nicht entladener Druckspeicher", "Fehlendes Entlastungsventil", "Wartungszugriff ohne Druckfreischaltung"},
+ TypicalHarm: "Quetsch- und Stossverletzungen durch unerwartete Zylinderbewegung",
+ RelevantLifecyclePhases: []string{"maintenance", "fault_finding", "decommissioning"},
+ RecommendedMeasuresDesign: []string{"Automatische Speicherentladung bei Hauptschalter-Aus", "Manuelles Entlastungsventil mit Druckanzeige"},
+ RecommendedMeasuresTechnical: []string{"Druckmanometer am Speicher", "Verriegeltes Entlastungsventil"},
+ RecommendedMeasuresInformation: []string{"Warnschild Druckspeicher", "LOTO-Verfahren fuer Druckspeicher"},
+ SuggestedEvidence: []string{"Funktionstest Speicherentladung", "Risikobeurteilung"},
+ RelatedKeywords: []string{"Druckspeicher", "Restenergie", "Speicherentladung"},
+ IsBuiltin: true,
+ TenantID: nil,
+ CreatedAt: now,
+ },
+ {
+ ID: hazardUUID("pneumatic_hydraulic", 5),
+ Category: "pneumatic_hydraulic",
+ SubCategory: "oelkontamination",
+ Name: "Kontamination von Hydraulikoel durch Partikel",
+ Description: "Verunreinigungen im Hydraulikoel fuehren zu erhoehtem Verschleiss an Ventilen und Dichtungen, was Leckagen und Funktionsversagen ausloest.",
+ DefaultSeverity: 3,
+ DefaultProbability: 3,
+ DefaultExposure: 3,
+ DefaultAvoidance: 4,
+ ApplicableComponentTypes: []string{"actuator", "mechanical"},
+ RegulationReferences: []string{"Maschinenverordnung 2023/1230 Anhang I"},
+ SuggestedMitigations: mustMarshalJSON([]string{"Feinfilterung des Hydraulikoels", "Regelmaessige Oelanalyse"}),
+ TypicalCauses: []string{"Verschleisspartikel im System", "Verschmutzte Nachfuellung", "Defekte Filterelemente"},
+ TypicalHarm: "Maschinenausfall mit Folgeverletzungen durch ploetzliches Versagen hydraulischer Funktionen",
+ RelevantLifecyclePhases: []string{"normal_operation", "maintenance"},
+ RecommendedMeasuresDesign: []string{"Mehrfachfiltration mit Bypass-Anzeige", "Geschlossener Nachfuellkreislauf"},
+ RecommendedMeasuresTechnical: []string{"Online-Partikelzaehler", "Differenzdruckanzeige am Filter"},
+ RecommendedMeasuresInformation: []string{"Oelwechselintervalle festlegen", "Sauberkeitsvorgaben fuer Nachfuellung"},
+ SuggestedEvidence: []string{"Oelanalysebericht", "Filterwechselprotokoll"},
+ RelatedKeywords: []string{"Hydraulikoel", "Kontamination", "Filtration"},
+ IsBuiltin: true,
+ TenantID: nil,
+ CreatedAt: now,
+ },
+ {
+ ID: hazardUUID("pneumatic_hydraulic", 6),
+ Category: "pneumatic_hydraulic",
+ SubCategory: "leckage",
+ Name: "Leckage an Hochdruckverbindungen",
+ Description: "Undichte Verschraubungen oder Dichtungen an Hochdruckverbindungen fuehren zu Medienaustritt, Rutschgefahr und moeglichen Hochdruckinjektionsverletzungen.",
+ DefaultSeverity: 4,
+ DefaultProbability: 3,
+ DefaultExposure: 3,
+ DefaultAvoidance: 3,
+ ApplicableComponentTypes: []string{"mechanical", "actuator"},
+ RegulationReferences: []string{"Maschinenverordnung 2023/1230 Anhang I"},
+ SuggestedMitigations: mustMarshalJSON([]string{"Leckagefreie Verschraubungen verwenden", "Auffangwannen unter Verbindungsstellen"}),
+ TypicalCauses: []string{"Vibrationsbedingte Lockerung", "Alterung der Dichtungen", "Falsches Anzugsmoment"},
+ TypicalHarm: "Rutschverletzungen, Hochdruckinjektion bei feinem Oelstrahl",
+ RelevantLifecyclePhases: []string{"normal_operation", "maintenance"},
+ RecommendedMeasuresDesign: []string{"Verschraubungen mit Sicherungsmitteln", "Leckage-Auffangvorrichtungen"},
+ RecommendedMeasuresTechnical: []string{"Fuellstandsueberwachung im Tank", "Leckagesensor"},
+ RecommendedMeasuresInformation: []string{"Sichtpruefung in Wartungsplan aufnehmen", "Hinweis auf Injektionsgefahr"},
+ SuggestedEvidence: []string{"Leckagepruefprotokoll", "Risikobeurteilung"},
+ RelatedKeywords: []string{"Leckage", "Verschraubung", "Hochdruck"},
+ IsBuiltin: true,
+ TenantID: nil,
+ CreatedAt: now,
+ },
+ {
+ ID: hazardUUID("pneumatic_hydraulic", 7),
+ Category: "pneumatic_hydraulic",
+ SubCategory: "kavitation",
+ Name: "Kavitation in Hydraulikpumpe",
+ Description: "Dampfblasenbildung und deren Implosion in der Hydraulikpumpe fuehren zu Materialabtrag, Leistungsverlust und ploetzlichem Pumpenversagen.",
+ DefaultSeverity: 3,
+ DefaultProbability: 2,
+ DefaultExposure: 3,
+ DefaultAvoidance: 4,
+ ApplicableComponentTypes: []string{"actuator", "mechanical"},
+ RegulationReferences: []string{"Maschinenverordnung 2023/1230 Anhang I"},
+ SuggestedMitigations: mustMarshalJSON([]string{"Korrekte Saughoehe einhalten", "Saugleitungsdimensionierung pruefen"}),
+ TypicalCauses: []string{"Zu kleine Saugleitung", "Verstopfter Saugfilter", "Zu hohe Oelviskositaet bei Kaelte"},
+ TypicalHarm: "Maschinenausfall durch Pumpenversagen mit moeglichen Folgeverletzungen",
+ RelevantLifecyclePhases: []string{"normal_operation", "setup"},
+ RecommendedMeasuresDesign: []string{"Saugleitung grosszuegig dimensionieren", "Ueberdruck-Zulaufsystem"},
+ RecommendedMeasuresTechnical: []string{"Vakuumanzeige an der Saugseite", "Temperaturueberwachung des Oels"},
+ RecommendedMeasuresInformation: []string{"Vorwaermverfahren bei Kaeltestart", "Wartungsintervall Saugfilter"},
+ SuggestedEvidence: []string{"Saugdruckmessung", "Pumpeninspektionsbericht"},
+ RelatedKeywords: []string{"Kavitation", "Hydraulikpumpe", "Saugleitung"},
+ IsBuiltin: true,
+ TenantID: nil,
+ CreatedAt: now,
+ },
+ {
+ ID: hazardUUID("pneumatic_hydraulic", 8),
+ Category: "pneumatic_hydraulic",
+ SubCategory: "ueberdruckversagen",
+ Name: "Ueberdruckversagen durch defektes Druckbegrenzungsventil",
+ Description: "Ein klemmendes oder falsch eingestelltes Druckbegrenzungsventil laesst den Systemdruck unkontrolliert ansteigen, was zum Bersten von Komponenten fuehren kann.",
+ DefaultSeverity: 5,
+ DefaultProbability: 2,
+ DefaultExposure: 3,
+ DefaultAvoidance: 2,
+ ApplicableComponentTypes: []string{"actuator", "mechanical"},
+ RegulationReferences: []string{"Maschinenverordnung 2023/1230 Anhang I"},
+ SuggestedMitigations: mustMarshalJSON([]string{"Redundantes Druckbegrenzungsventil", "Druckschalter mit Abschaltung"}),
+ TypicalCauses: []string{"Verschmutztes Druckbegrenzungsventil", "Falsche Einstellung nach Wartung", "Ermuedung der Ventilfeder"},
+ TypicalHarm: "Bersten von Leitungen und Gehaeusen mit Splitterwurf, Hochdruckinjektionsverletzungen",
+ RelevantLifecyclePhases: []string{"normal_operation", "maintenance"},
+ RecommendedMeasuresDesign: []string{"Redundante Druckbegrenzung", "Berstscheibe als letzte Sicherung"},
+ RecommendedMeasuresTechnical: []string{"Druckschalter mit sicherer Pumpenabschaltung", "Manometer mit Schleppzeiger"},
+ RecommendedMeasuresInformation: []string{"Pruefintervall Druckbegrenzungsventil", "Einstellprotokoll nach Wartung"},
+ SuggestedEvidence: []string{"Ventilpruefprotokoll", "Druckverlaufsmessung"},
+ RelatedKeywords: []string{"Ueberdruck", "Druckbegrenzungsventil", "Bersten"},
+ IsBuiltin: true,
+ TenantID: nil,
+ CreatedAt: now,
+ },
+ {
+ ID: hazardUUID("pneumatic_hydraulic", 9),
+ Category: "pneumatic_hydraulic",
+ SubCategory: "ventilversagen",
+ Name: "Unkontrollierte Zylinderbewegung bei Ventilversagen",
+ Description: "Bei Ausfall oder Fehlfunktion eines Wegeventils kann ein Zylinder unkontrolliert ein- oder ausfahren und Personen im Bewegungsbereich verletzen.",
+ DefaultSeverity: 5,
+ DefaultProbability: 2,
+ DefaultExposure: 3,
+ DefaultAvoidance: 2,
+ ApplicableComponentTypes: []string{"actuator", "controller"},
+ RegulationReferences: []string{"Maschinenverordnung 2023/1230 Anhang I"},
+ SuggestedMitigations: mustMarshalJSON([]string{"Redundante Ventile fuer sicherheitskritische Achsen", "Lasthalteventile an Vertikalachsen"}),
+ TypicalCauses: []string{"Elektromagnetausfall am Ventil", "Ventilschieber klemmt", "Kontamination blockiert Ventilsitz"},
+ TypicalHarm: "Quetsch- und Stossverletzungen durch unkontrollierte Zylinderbewegung",
+ RelevantLifecyclePhases: []string{"normal_operation", "fault_finding"},
+ RecommendedMeasuresDesign: []string{"Redundante Ventilanordnung mit Ueberwachung", "Lasthalteventile fuer schwerkraftbelastete Achsen"},
+ RecommendedMeasuresTechnical: []string{"Positionsueberwachung am Zylinder", "Ventil-Stellungsueberwachung"},
+ RecommendedMeasuresInformation: []string{"Fehlermeldung bei Ventildiskrepanz", "Notfallprozedur bei Ventilversagen"},
+ SuggestedEvidence: []string{"Funktionstest Redundanz", "FMEA Ventilschaltung"},
+ RelatedKeywords: []string{"Wegeventil", "Zylinderversagen", "Ventilausfall"},
+ IsBuiltin: true,
+ TenantID: nil,
+ CreatedAt: now,
+ },
+ {
+ ID: hazardUUID("pneumatic_hydraulic", 10),
+ Category: "pneumatic_hydraulic",
+ SubCategory: "viskositaet",
+ Name: "Temperaturbedingte Viskositaetsaenderung von Hydraulikmedium",
+ Description: "Extreme Temperaturen veraendern die Viskositaet des Hydraulikoels so stark, dass Ventile und Pumpen nicht mehr zuverlaessig arbeiten und Sicherheitsfunktionen versagen.",
+ DefaultSeverity: 3,
+ DefaultProbability: 2,
+ DefaultExposure: 2,
+ DefaultAvoidance: 4,
+ ApplicableComponentTypes: []string{"actuator", "mechanical"},
+ RegulationReferences: []string{"Maschinenverordnung 2023/1230 Anhang I"},
+ SuggestedMitigations: mustMarshalJSON([]string{"Oeltemperierung", "Oelsorte mit breitem Viskositaetsbereich"}),
+ TypicalCauses: []string{"Kaltstart ohne Vorwaermung", "Ueberhitzung durch mangelnde Kuehlung", "Falsche Oelsorte"},
+ TypicalHarm: "Funktionsversagen hydraulischer Sicherheitseinrichtungen",
+ RelevantLifecyclePhases: []string{"normal_operation", "setup"},
+ RecommendedMeasuresDesign: []string{"Oelkuehler und Oelheizung vorsehen", "Temperaturbereich der Oelsorte abstimmen"},
+ RecommendedMeasuresTechnical: []string{"Oeltemperatursensor mit Warnmeldung", "Aufwaermprogramm in der Steuerung"},
+ RecommendedMeasuresInformation: []string{"Zulaessiger Temperaturbereich in Betriebsanleitung", "Oelwechselvorschrift"},
+ SuggestedEvidence: []string{"Temperaturverlaufsmessung", "Oeldatenblatt"},
+ RelatedKeywords: []string{"Viskositaet", "Oeltemperatur", "Hydraulikmedium"},
+ IsBuiltin: true,
+ TenantID: nil,
+ CreatedAt: now,
+ },
+ // ====================================================================
+ // Category: noise_vibration (indices 1-6, 6 entries)
+ // ====================================================================
+ {
+ ID: hazardUUID("noise_vibration", 1),
+ Category: "noise_vibration",
+ SubCategory: "dauerschall",
+ Name: "Gehoerschaedigung durch Dauerschallpegel",
+ Description: "Dauerhaft erhoehte Schallpegel am Arbeitsplatz ueber dem Grenzwert fuehren zu irreversiblen Gehoerschaeden bei den Maschinenbedienern.",
+ DefaultSeverity: 4,
+ DefaultProbability: 4,
+ DefaultExposure: 4,
+ DefaultAvoidance: 3,
+ ApplicableComponentTypes: []string{"mechanical", "actuator"},
+ RegulationReferences: []string{"Maschinenverordnung 2023/1230 Anhang I"},
+ SuggestedMitigations: mustMarshalJSON([]string{"Laermminderung an der Quelle", "Gehoerschutzpflicht ab 85 dB(A)"}),
+ TypicalCauses: []string{"Nicht gekapselte Antriebe", "Metallische Schlagvorgaenge", "Fehlende Schalldaemmung"},
+ TypicalHarm: "Laermschwerhoerigkeit, Tinnitus",
+ RelevantLifecyclePhases: []string{"normal_operation"},
+ RecommendedMeasuresDesign: []string{"Laermarme Antriebe und Getriebe", "Schwingungsdaempfende Lagerung"},
+ RecommendedMeasuresTechnical: []string{"Schallschutzkapseln", "Schallschutzwaende"},
+ RecommendedMeasuresInformation: []string{"Laermbereichskennzeichnung", "Gehoerschutzpflicht beschildern"},
+ SuggestedEvidence: []string{"Laermpegelmessung am Arbeitsplatz", "Laermkataster"},
+ RelatedKeywords: []string{"Laerm", "Gehoerschutz", "Schallpegel"},
+ IsBuiltin: true,
+ TenantID: nil,
+ CreatedAt: now,
+ },
+ {
+ ID: hazardUUID("noise_vibration", 2),
+ Category: "noise_vibration",
+ SubCategory: "hand_arm_vibration",
+ Name: "Hand-Arm-Vibrationssyndrom durch vibrierende Werkzeuge",
+ Description: "Langzeitige Nutzung handgefuehrter vibrierender Werkzeuge kann zu Durchblutungsstoerungen, Nervenschaeden und Gelenkbeschwerden in Haenden und Armen fuehren.",
+ DefaultSeverity: 4,
+ DefaultProbability: 3,
+ DefaultExposure: 4,
+ DefaultAvoidance: 3,
+ ApplicableComponentTypes: []string{"mechanical", "other"},
+ RegulationReferences: []string{"Maschinenverordnung 2023/1230 Anhang I"},
+ SuggestedMitigations: mustMarshalJSON([]string{"Vibrationsgedaempfte Werkzeuge verwenden", "Expositionszeit begrenzen"}),
+ TypicalCauses: []string{"Ungepufferte Handgriffe", "Verschlissene Werkzeuge mit erhoehter Vibration", "Fehlende Arbeitszeitbegrenzung"},
+ TypicalHarm: "Weissfingerkrankheit, Karpaltunnelsyndrom, Gelenkarthrose",
+ RelevantLifecyclePhases: []string{"normal_operation", "maintenance"},
+ RecommendedMeasuresDesign: []string{"Vibrationsgedaempfte Griffe", "Automatisierung statt Handarbeit"},
+ RecommendedMeasuresTechnical: []string{"Vibrationsmessung am Werkzeug", "Anti-Vibrationshandschuhe"},
+ RecommendedMeasuresInformation: []string{"Expositionsdauer dokumentieren", "Arbeitsmedizinische Vorsorge anbieten"},
+ SuggestedEvidence: []string{"Vibrationsmessung nach ISO 5349", "Expositionsberechnung"},
+ RelatedKeywords: []string{"Vibration", "Hand-Arm", "HAVS"},
+ IsBuiltin: true,
+ TenantID: nil,
+ CreatedAt: now,
+ },
+ {
+ ID: hazardUUID("noise_vibration", 3),
+ Category: "noise_vibration",
+ SubCategory: "ganzkoerpervibration",
+ Name: "Ganzkoerpervibration an Bedienplaetzen",
+ Description: "Vibrationen, die ueber den Sitz oder die Standflaeche auf den gesamten Koerper uebertragen werden, koennen zu Wirbelsaeulenschaeden fuehren.",
+ DefaultSeverity: 3,
+ DefaultProbability: 3,
+ DefaultExposure: 4,
+ DefaultAvoidance: 3,
+ ApplicableComponentTypes: []string{"mechanical", "other"},
+ RegulationReferences: []string{"Maschinenverordnung 2023/1230 Anhang I"},
+ SuggestedMitigations: mustMarshalJSON([]string{"Schwingungsisolierter Fahrersitz", "Vibrationsgedaempfte Stehplattform"}),
+ TypicalCauses: []string{"Unwucht in rotierenden Teilen", "Unebener Fahrweg", "Fehlende Schwingungsisolierung des Bedienplatzes"},
+ TypicalHarm: "Bandscheibenschaeden, Rueckenschmerzen, Ermuedung",
+ RelevantLifecyclePhases: []string{"normal_operation"},
+ RecommendedMeasuresDesign: []string{"Schwingungsisolierte Kabine oder Plattform", "Auswuchten rotierender Massen"},
+ RecommendedMeasuresTechnical: []string{"Luftgefederter Sitz", "Vibrationsueberwachung mit Grenzwertwarnung"},
+ RecommendedMeasuresInformation: []string{"Maximalexpositionsdauer festlegen", "Arbeitsmedizinische Vorsorge"},
+ SuggestedEvidence: []string{"Ganzkoerper-Vibrationsmessung nach ISO 2631", "Expositionsbewertung"},
+ RelatedKeywords: []string{"Ganzkoerpervibration", "Wirbelsaeule", "Sitzvibrationen"},
+ IsBuiltin: true,
+ TenantID: nil,
+ CreatedAt: now,
+ },
+ {
+ ID: hazardUUID("noise_vibration", 4),
+ Category: "noise_vibration",
+ SubCategory: "impulslaerm",
+ Name: "Impulslaerm durch Stanz-/Praegevorgaenge",
+ Description: "Kurzzeitige Schallspitzen bei Stanz-, Praege- oder Nietvorgaengen ueberschreiten den Spitzenschalldruckpegel und schaedigen das Gehoer besonders stark.",
+ DefaultSeverity: 4,
+ DefaultProbability: 4,
+ DefaultExposure: 3,
+ DefaultAvoidance: 3,
+ ApplicableComponentTypes: []string{"mechanical"},
+ RegulationReferences: []string{"Maschinenverordnung 2023/1230 Anhang I"},
+ SuggestedMitigations: mustMarshalJSON([]string{"Schalldaemmende Werkzeugeinhausung", "Impulsschallgedaempfter Gehoerschutz"}),
+ TypicalCauses: []string{"Metall-auf-Metall-Schlag", "Offene Stanzwerkzeuge", "Fehlende Schalldaemmung"},
+ TypicalHarm: "Akutes Knalltrauma, irreversible Gehoerschaedigung",
+ RelevantLifecyclePhases: []string{"normal_operation"},
+ RecommendedMeasuresDesign: []string{"Elastische Werkzeugauflagen", "Geschlossene Werkzeugkammer"},
+ RecommendedMeasuresTechnical: []string{"Schallschutzkabine um Stanzbereich", "Impulslaermueberwachung"},
+ RecommendedMeasuresInformation: []string{"Gehoerschutzpflicht-Kennzeichnung", "Schulung zur Impulslaermgefahr"},
+ SuggestedEvidence: []string{"Spitzenpegelmessung", "Laermgutachten"},
+ RelatedKeywords: []string{"Impulslaerm", "Stanzen", "Spitzenschallpegel"},
+ IsBuiltin: true,
+ TenantID: nil,
+ CreatedAt: now,
+ },
+ {
+ ID: hazardUUID("noise_vibration", 5),
+ Category: "noise_vibration",
+ SubCategory: "infraschall",
+ Name: "Infraschall von Grossventilatoren",
+ Description: "Grosse Ventilatoren und Geblaese erzeugen niederfrequenten Infraschall, der zu Unwohlsein, Konzentrationsstoerungen und Ermuedung fuehren kann.",
+ DefaultSeverity: 3,
+ DefaultProbability: 2,
+ DefaultExposure: 3,
+ DefaultAvoidance: 3,
+ ApplicableComponentTypes: []string{"mechanical", "actuator"},
+ RegulationReferences: []string{"Maschinenverordnung 2023/1230 Anhang I"},
+ SuggestedMitigations: mustMarshalJSON([]string{"Schwingungsisolierte Aufstellung", "Schalldaempfer in Kanaelen"}),
+ TypicalCauses: []string{"Grosse Ventilatorschaufeln mit niedriger Drehzahl", "Resonanzen in Luftkanaelen", "Fehlende Schwingungsentkopplung"},
+ TypicalHarm: "Unwohlsein, Uebelkeit, Konzentrationsstoerungen bei Dauerexposition",
+ RelevantLifecyclePhases: []string{"normal_operation"},
+ RecommendedMeasuresDesign: []string{"Schwingungsentkopplung des Ventilators", "Resonanzfreie Kanaldimensionierung"},
+ RecommendedMeasuresTechnical: []string{"Niederfrequenz-Schalldaempfer", "Infraschall-Messgeraet"},
+ RecommendedMeasuresInformation: []string{"Aufklaerung ueber Infraschallsymptome", "Expositionshinweise"},
+ SuggestedEvidence: []string{"Infraschallmessung", "Risikobeurteilung"},
+ RelatedKeywords: []string{"Infraschall", "Ventilator", "Niederfrequenz"},
+ IsBuiltin: true,
+ TenantID: nil,
+ CreatedAt: now,
+ },
+ {
+ ID: hazardUUID("noise_vibration", 6),
+ Category: "noise_vibration",
+ SubCategory: "resonanz",
+ Name: "Resonanzschwingungen in Maschinengestell",
+ Description: "Anregung des Maschinengestells in seiner Eigenfrequenz kann zu unkontrollierten Schwingungen fuehren, die Bauteile ermueden und zum Versagen bringen.",
+ DefaultSeverity: 4,
+ DefaultProbability: 2,
+ DefaultExposure: 3,
+ DefaultAvoidance: 3,
+ ApplicableComponentTypes: []string{"mechanical"},
+ RegulationReferences: []string{"Maschinenverordnung 2023/1230 Anhang I"},
+ SuggestedMitigations: mustMarshalJSON([]string{"Eigenfrequenzanalyse bei Konstruktion", "Schwingungsdaempfer anbringen"}),
+ TypicalCauses: []string{"Drehzahl nahe der Eigenfrequenz des Gestells", "Fehlende Daempfungselemente", "Nachtraegliche Massenveraenderungen"},
+ TypicalHarm: "Materialermuedungsbruch mit Absturz von Bauteilen, Verletzungen durch Bruchstuecke",
+ RelevantLifecyclePhases: []string{"normal_operation", "setup"},
+ RecommendedMeasuresDesign: []string{"Eigenfrequenz ausserhalb des Betriebsdrehzahlbereichs legen", "Versteifung des Gestells"},
+ RecommendedMeasuresTechnical: []string{"Schwingungssensoren mit Grenzwertueberwachung", "Tilger oder Daempfer anbringen"},
+ RecommendedMeasuresInformation: []string{"Verbotene Drehzahlbereiche kennzeichnen", "Schwingungsueberwachungsanleitung"},
+ SuggestedEvidence: []string{"Modalanalyse des Gestells", "Schwingungsmessprotokoll"},
+ RelatedKeywords: []string{"Resonanz", "Eigenfrequenz", "Strukturschwingung"},
+ IsBuiltin: true,
+ TenantID: nil,
+ CreatedAt: now,
+ },
+ }
+}
diff --git a/ai-compliance-sdk/internal/iace/hazard_library_machine_safety.go b/ai-compliance-sdk/internal/iace/hazard_library_machine_safety.go
new file mode 100644
index 0000000..67fe11b
--- /dev/null
+++ b/ai-compliance-sdk/internal/iace/hazard_library_machine_safety.go
@@ -0,0 +1,597 @@
+package iace
+
+import "time"
+
+// builtinHazardsMachineSafety returns hazard library entries covering
+// mechanical, environmental and machine safety hazards.
+func builtinHazardsMachineSafety() []HazardLibraryEntry {
+ now := time.Now()
+ return []HazardLibraryEntry{
+ // Category: mechanical_hazard (6 entries)
+ // ====================================================================
+ {
+ ID: hazardUUID("mechanical_hazard", 1),
+ Category: "mechanical_hazard",
+ Name: "Unerwarteter Anlauf nach Spannungsausfall",
+ Description: "Nach Wiederkehr der Versorgungsspannung laeuft die Maschine unerwartet an, ohne dass eine Startfreigabe durch den Bediener erfolgt ist.",
+ DefaultSeverity: 5,
+ DefaultProbability: 3,
+ ApplicableComponentTypes: []string{"controller", "firmware"},
+ RegulationReferences: []string{"Maschinenverordnung 2023/1230 Anhang I §1.2.6", "ISO 13849"},
+ SuggestedMitigations: mustMarshalJSON([]string{"Anlaufschutz", "Anti-Restart-Funktion", "Sicherheitsrelais"}),
+ IsBuiltin: true,
+ TenantID: nil,
+ CreatedAt: now,
+ },
+ {
+ ID: hazardUUID("mechanical_hazard", 2),
+ Category: "mechanical_hazard",
+ Name: "Restenergie nach Abschalten",
+ Description: "Gespeicherte kinetische oder potentielle Energie (z.B. Schwungmasse, abgesenktes Werkzeug) wird nach dem Abschalten nicht sicher abgebaut.",
+ DefaultSeverity: 5,
+ DefaultProbability: 2,
+ ApplicableComponentTypes: []string{"actuator", "controller"},
+ RegulationReferences: []string{"Maschinenverordnung 2023/1230 Anhang I §1.6", "ISO 13849"},
+ SuggestedMitigations: mustMarshalJSON([]string{"Energieabbau-Prozedur", "Mechanische Haltevorrichtung", "LOTO-Freischaltung"}),
+ IsBuiltin: true,
+ TenantID: nil,
+ CreatedAt: now,
+ },
+ {
+ ID: hazardUUID("mechanical_hazard", 3),
+ Category: "mechanical_hazard",
+ Name: "Unerwartete Maschinenbewegung",
+ Description: "Die Maschine fuehrt eine unkontrollierte Bewegung durch (z.B. Antrieb faehrt ohne Kommando los), was Personen im Gefahrenbereich verletzt.",
+ DefaultSeverity: 5,
+ DefaultProbability: 2,
+ ApplicableComponentTypes: []string{"actuator", "controller", "software"},
+ RegulationReferences: []string{"Maschinenverordnung 2023/1230", "ISO 13849", "IEC 62061"},
+ SuggestedMitigations: mustMarshalJSON([]string{"Safe Torque Off (STO)", "Geschwindigkeitsueberwachung", "Schutzzaun-Sensorik"}),
+ IsBuiltin: true,
+ TenantID: nil,
+ CreatedAt: now,
+ },
+ {
+ ID: hazardUUID("mechanical_hazard", 4),
+ Category: "mechanical_hazard",
+ Name: "Teileauswurf durch Fehlfunktion",
+ Description: "Werkzeuge, Werkstuecke oder Maschinenteile werden bei einer Fehlfunktion unkontrolliert aus der Maschine geschleudert.",
+ DefaultSeverity: 5,
+ DefaultProbability: 2,
+ ApplicableComponentTypes: []string{"actuator", "controller"},
+ RegulationReferences: []string{"Maschinenverordnung 2023/1230 Anhang I §1.3.2"},
+ SuggestedMitigations: mustMarshalJSON([]string{"Trennende Schutzeinrichtung", "Sicherheitsglas", "Spannkraft-Ueberwachung"}),
+ IsBuiltin: true,
+ TenantID: nil,
+ CreatedAt: now,
+ },
+ {
+ ID: hazardUUID("mechanical_hazard", 5),
+ Category: "mechanical_hazard",
+ Name: "Quetschstelle durch fehlende Schutzeinrichtung",
+ Description: "Zwischen beweglichen und festen Maschinenteilen entstehen Quetschstellen, die bei fehlendem Schutz zu schweren Verletzungen fuehren koennen.",
+ DefaultSeverity: 5,
+ DefaultProbability: 2,
+ ApplicableComponentTypes: []string{"actuator"},
+ RegulationReferences: []string{"Maschinenverordnung 2023/1230 Anhang I §1.3.7", "ISO 13857"},
+ SuggestedMitigations: mustMarshalJSON([]string{"Schutzverkleidung", "Sicherheitsabstaende nach ISO 13857", "Lichtvorhang"}),
+ IsBuiltin: true,
+ TenantID: nil,
+ CreatedAt: now,
+ },
+ {
+ ID: hazardUUID("mechanical_hazard", 6),
+ Category: "mechanical_hazard",
+ Name: "Instabile Struktur unter Last",
+ Description: "Die Maschinenstruktur oder ein Anbauteil versagt unter statischer oder dynamischer Belastung und gefaehrdet Personen in der Naehe.",
+ DefaultSeverity: 5,
+ DefaultProbability: 1,
+ ApplicableComponentTypes: []string{"other"},
+ RegulationReferences: []string{"Maschinenverordnung 2023/1230 Anhang I §1.3.1"},
+ SuggestedMitigations: mustMarshalJSON([]string{"Festigkeitsberechnung", "Ueberlastsicherung", "Regelmaessige Inspektion"}),
+ IsBuiltin: true,
+ TenantID: nil,
+ CreatedAt: now,
+ },
+
+ // ====================================================================
+ // Category: electrical_hazard (6 entries)
+ // ====================================================================
+ {
+ ID: hazardUUID("electrical_hazard", 1),
+ Category: "electrical_hazard",
+ Name: "Elektrischer Schlag an Bedienpanel",
+ Description: "Bediener kommen mit spannungsfuehrenden Teilen in Beruehrung, z.B. durch defekte Gehaeuseerdung oder fehlerhafte Isolierung.",
+ DefaultSeverity: 5,
+ DefaultProbability: 2,
+ ApplicableComponentTypes: []string{"hmi", "controller"},
+ RegulationReferences: []string{"Niederspannungsrichtlinie 2014/35/EU", "IEC 60204-1"},
+ SuggestedMitigations: mustMarshalJSON([]string{"Schutzkleinspannung (SELV)", "Schutzerdung", "Isolationsmonitoring"}),
+ IsBuiltin: true,
+ TenantID: nil,
+ CreatedAt: now,
+ },
+ {
+ ID: hazardUUID("electrical_hazard", 2),
+ Category: "electrical_hazard",
+ Name: "Lichtbogen durch Schaltfehler",
+ Description: "Ein Schaltfehler erzeugt einen Lichtbogen, der Bediener verletzen, Geraete beschaedigen oder einen Brand ausloesen kann.",
+ DefaultSeverity: 5,
+ DefaultProbability: 1,
+ ApplicableComponentTypes: []string{"controller"},
+ RegulationReferences: []string{"Niederspannungsrichtlinie 2014/35/EU", "IEC 60204-1"},
+ SuggestedMitigations: mustMarshalJSON([]string{"Lichtbogenschutz-Schalter", "Kurzschlussschutz", "Geeignete Schaltgeraete"}),
+ IsBuiltin: true,
+ TenantID: nil,
+ CreatedAt: now,
+ },
+ {
+ ID: hazardUUID("electrical_hazard", 3),
+ Category: "electrical_hazard",
+ Name: "Kurzschluss durch Isolationsfehler",
+ Description: "Beschaedigte Kabelisolierungen fuehren zu einem Kurzschluss, der Feuer ausloesen oder Sicherheitsfunktionen ausser Betrieb setzen kann.",
+ DefaultSeverity: 4,
+ DefaultProbability: 2,
+ ApplicableComponentTypes: []string{"network", "controller"},
+ RegulationReferences: []string{"Niederspannungsrichtlinie 2014/35/EU", "IEC 60204-1"},
+ SuggestedMitigations: mustMarshalJSON([]string{"Isolationsueberwachung", "Kabelschutz", "Regelmaessige Sichtpruefung"}),
+ IsBuiltin: true,
+ TenantID: nil,
+ CreatedAt: now,
+ },
+ {
+ ID: hazardUUID("electrical_hazard", 4),
+ Category: "electrical_hazard",
+ Name: "Erdschluss in Steuerkreis",
+ Description: "Ein Erdschluss im Steuerkreis kann unbeabsichtigte Schaltzustaende ausloesen und Sicherheitsfunktionen beeinflussen.",
+ DefaultSeverity: 4,
+ DefaultProbability: 2,
+ ApplicableComponentTypes: []string{"controller", "network"},
+ RegulationReferences: []string{"IEC 60204-1", "ISO 13849"},
+ SuggestedMitigations: mustMarshalJSON([]string{"Erdschluss-Monitoring", "IT-Netz fuer Steuerkreise", "Regelmaessige Pruefung"}),
+ IsBuiltin: true,
+ TenantID: nil,
+ CreatedAt: now,
+ },
+ {
+ ID: hazardUUID("electrical_hazard", 5),
+ Category: "electrical_hazard",
+ Name: "Gespeicherte Energie in Kondensatoren",
+ Description: "Nach dem Abschalten verbleiben hohe Spannungen in Kondensatoren (z.B. Frequenzumrichter, USV), was bei Wartungsarbeiten gefaehrlich ist.",
+ DefaultSeverity: 5,
+ DefaultProbability: 2,
+ ApplicableComponentTypes: []string{"controller"},
+ RegulationReferences: []string{"Niederspannungsrichtlinie 2014/35/EU", "IEC 60204-1"},
+ SuggestedMitigations: mustMarshalJSON([]string{"Entladewartezeit", "Automatische Entladeschaltung", "Warnhinweise am Geraet"}),
+ IsBuiltin: true,
+ TenantID: nil,
+ CreatedAt: now,
+ },
+ {
+ ID: hazardUUID("electrical_hazard", 6),
+ Category: "electrical_hazard",
+ Name: "Elektromagnetische Kopplung auf Safety-Leitung",
+ Description: "Hochfrequente Stoerfelder koppeln auf ungeschirmte Safety-Leitungen und erzeugen Falschsignale, die Sicherheitsfunktionen fehl ausloesen.",
+ DefaultSeverity: 3,
+ DefaultProbability: 3,
+ ApplicableComponentTypes: []string{"network", "sensor"},
+ RegulationReferences: []string{"EMV-Richtlinie 2014/30/EU", "IEC 62061"},
+ SuggestedMitigations: mustMarshalJSON([]string{"Geschirmte Kabel", "Raeumliche Trennung", "EMV-Pruefung"}),
+ IsBuiltin: true,
+ TenantID: nil,
+ CreatedAt: now,
+ },
+
+ // ====================================================================
+ // Category: thermal_hazard (4 entries)
+ // ====================================================================
+ {
+ ID: hazardUUID("thermal_hazard", 1),
+ Category: "thermal_hazard",
+ Name: "Ueberhitzung der Steuereinheit",
+ Description: "Die Steuereinheit ueberschreitet die zulaessige Betriebstemperatur durch Lueftungsausfall oder hohe Umgebungstemperatur, was zu Fehlfunktionen fuehrt.",
+ DefaultSeverity: 3,
+ DefaultProbability: 3,
+ ApplicableComponentTypes: []string{"controller", "firmware"},
+ RegulationReferences: []string{"Maschinenverordnung 2023/1230", "IEC 60068"},
+ SuggestedMitigations: mustMarshalJSON([]string{"Temperaturueberwachung", "Redundante Lueftung", "Thermisches Abschalten"}),
+ IsBuiltin: true,
+ TenantID: nil,
+ CreatedAt: now,
+ },
+ {
+ ID: hazardUUID("thermal_hazard", 2),
+ Category: "thermal_hazard",
+ Name: "Brandgefahr durch Leistungselektronik",
+ Description: "Defekte Leistungshalbleiter oder Kondensatoren in der Leistungselektronik erwaermen sich unkontrolliert und koennen einen Brand ausloesen.",
+ DefaultSeverity: 5,
+ DefaultProbability: 2,
+ ApplicableComponentTypes: []string{"controller"},
+ RegulationReferences: []string{"Niederspannungsrichtlinie 2014/35/EU", "IEC 60204-1"},
+ SuggestedMitigations: mustMarshalJSON([]string{"Thermosicherungen", "Temperatursensoren", "Brandschutzmassnahmen"}),
+ IsBuiltin: true,
+ TenantID: nil,
+ CreatedAt: now,
+ },
+ {
+ ID: hazardUUID("thermal_hazard", 3),
+ Category: "thermal_hazard",
+ Name: "Einfrieren bei Tieftemperatur",
+ Description: "Sehr tiefe Umgebungstemperaturen fuehren zum Einfrieren von Hydraulikleitungen oder Elektronik und damit zum Ausfall von Sicherheitsfunktionen.",
+ DefaultSeverity: 3,
+ DefaultProbability: 3,
+ ApplicableComponentTypes: []string{"controller", "actuator"},
+ RegulationReferences: []string{"Maschinenverordnung 2023/1230", "IEC 60068"},
+ SuggestedMitigations: mustMarshalJSON([]string{"Heizung", "Mindestbetriebstemperatur definieren", "Temperatursensor"}),
+ IsBuiltin: true,
+ TenantID: nil,
+ CreatedAt: now,
+ },
+ {
+ ID: hazardUUID("thermal_hazard", 4),
+ Category: "thermal_hazard",
+ Name: "Waermestress an Kabelisolierung",
+ Description: "Langfristige Einwirkung hoher Temperaturen auf Kabelisolierungen fuehrt zu Alterung und Isolationsversagen mit Kurzschlussrisiko.",
+ DefaultSeverity: 3,
+ DefaultProbability: 3,
+ ApplicableComponentTypes: []string{"network", "controller"},
+ RegulationReferences: []string{"IEC 60204-1", "Maschinenverordnung 2023/1230"},
+ SuggestedMitigations: mustMarshalJSON([]string{"Hitzebestaendige Kabel (z.B. PTFE)", "Kabelverlegung mit Abstand zur Waermequelle", "Regelmaessige Inspektion"}),
+ IsBuiltin: true,
+ TenantID: nil,
+ CreatedAt: now,
+ },
+
+ // ====================================================================
+ // Category: emc_hazard (5 entries)
+ // ====================================================================
+ {
+ ID: hazardUUID("emc_hazard", 1),
+ Category: "emc_hazard",
+ Name: "EMV-Stoerabstrahlung auf Safety-Bus",
+ Description: "Hohe elektromagnetische Stoerabstrahlung aus benachbarten Geraeten stoert den industriellen Safety-Bus (z.B. PROFIsafe) und erzeugt Kommunikationsfehler.",
+ DefaultSeverity: 5,
+ DefaultProbability: 2,
+ ApplicableComponentTypes: []string{"network", "controller"},
+ RegulationReferences: []string{"EMV-Richtlinie 2014/30/EU", "IEC 62061", "IEC 61784-3"},
+ SuggestedMitigations: mustMarshalJSON([]string{"EMV-gerechte Verkabelung", "Schirmung", "EMC-Pruefung nach EN 55011"}),
+ IsBuiltin: true,
+ TenantID: nil,
+ CreatedAt: now,
+ },
+ {
+ ID: hazardUUID("emc_hazard", 2),
+ Category: "emc_hazard",
+ Name: "Unbeabsichtigte elektromagnetische Abstrahlung",
+ Description: "Die Maschine selbst strahlt starke EM-Felder ab, die andere sicherheitsrelevante Einrichtungen in der Naehe stoeren.",
+ DefaultSeverity: 2,
+ DefaultProbability: 3,
+ ApplicableComponentTypes: []string{"controller", "actuator"},
+ RegulationReferences: []string{"EMV-Richtlinie 2014/30/EU"},
+ SuggestedMitigations: mustMarshalJSON([]string{"EMV-Filter", "Gehaeuseabschirmung", "CE-Zulassung Frequenzumrichter"}),
+ IsBuiltin: true,
+ TenantID: nil,
+ CreatedAt: now,
+ },
+ {
+ ID: hazardUUID("emc_hazard", 3),
+ Category: "emc_hazard",
+ Name: "Frequenzumrichter-Stoerung auf Steuerleitung",
+ Description: "Der Frequenzumrichter erzeugt hochfrequente Stoerungen, die auf benachbarte Steuerleitungen koppeln und falsche Signale erzeugen.",
+ DefaultSeverity: 4,
+ DefaultProbability: 3,
+ ApplicableComponentTypes: []string{"actuator", "network"},
+ RegulationReferences: []string{"EMV-Richtlinie 2014/30/EU", "IEC 60204-1"},
+ SuggestedMitigations: mustMarshalJSON([]string{"Motorfilter", "Kabeltrennabstand", "Separate Kabelkanaele"}),
+ IsBuiltin: true,
+ TenantID: nil,
+ CreatedAt: now,
+ },
+ {
+ ID: hazardUUID("emc_hazard", 4),
+ Category: "emc_hazard",
+ Name: "ESD-Schaden an Elektronik",
+ Description: "Elektrostatische Entladung bei Wartung oder Austausch beschaedigt empfindliche Elektronikbauteile, was zu latenten Fehlfunktionen fuehrt.",
+ DefaultSeverity: 3,
+ DefaultProbability: 3,
+ ApplicableComponentTypes: []string{"controller", "firmware"},
+ RegulationReferences: []string{"IEC 61000-4-2", "Maschinenverordnung 2023/1230"},
+ SuggestedMitigations: mustMarshalJSON([]string{"ESD-Schulung", "ESD-Schutzausruestung", "ESD-gerechte Verpackung"}),
+ IsBuiltin: true,
+ TenantID: nil,
+ CreatedAt: now,
+ },
+ {
+ ID: hazardUUID("emc_hazard", 5),
+ Category: "emc_hazard",
+ Name: "HF-Stoerung des Sicherheitssensors",
+ Description: "Hochfrequenz-Stoerquellen (z.B. Schweissgeraete, Mobiltelefone) beeinflussen die Funktion von Sicherheitssensoren (Lichtvorhang, Scanner).",
+ DefaultSeverity: 4,
+ DefaultProbability: 3,
+ ApplicableComponentTypes: []string{"sensor"},
+ RegulationReferences: []string{"EMV-Richtlinie 2014/30/EU", "IEC 61496"},
+ SuggestedMitigations: mustMarshalJSON([]string{"EMV-zertifizierte Sicherheitssensoren", "HF-Quellen trennen", "Gegensprechanlagenverbot in Gefahrenzone"}),
+ IsBuiltin: true,
+ TenantID: nil,
+ CreatedAt: now,
+ },
+
+ // ====================================================================
+ // Category: safety_function_failure (8 entries)
+ // ====================================================================
+ {
+ ID: hazardUUID("safety_function_failure", 1),
+ Category: "safety_function_failure",
+ Name: "Not-Halt trennt Energieversorgung nicht",
+ Description: "Der Not-Halt-Taster betaetigt die Sicherheitsschalter, die Energiezufuhr wird jedoch nicht vollstaendig unterbrochen, weil das Sicherheitsrelais versagt.",
+ DefaultSeverity: 5,
+ DefaultProbability: 2,
+ ApplicableComponentTypes: []string{"controller", "actuator"},
+ RegulationReferences: []string{"Maschinenverordnung 2023/1230 Anhang I §1.2.4", "IEC 60947-5-5", "ISO 13849"},
+ SuggestedMitigations: mustMarshalJSON([]string{"Regelmaessiger Not-Halt-Test", "Redundantes Sicherheitsrelais", "Selbstueberwachender Sicherheitskreis"}),
+ IsBuiltin: true,
+ TenantID: nil,
+ CreatedAt: now,
+ },
+ {
+ ID: hazardUUID("safety_function_failure", 2),
+ Category: "safety_function_failure",
+ Name: "Schutztuer-Monitoring umgangen",
+ Description: "Das Schutztuer-Positionssignal wird durch einen Fehler oder Manipulation als 'geschlossen' gemeldet, obwohl die Tuer offen ist.",
+ DefaultSeverity: 5,
+ DefaultProbability: 2,
+ ApplicableComponentTypes: []string{"sensor", "controller"},
+ RegulationReferences: []string{"Maschinenverordnung 2023/1230", "ISO 14119", "ISO 13849"},
+ SuggestedMitigations: mustMarshalJSON([]string{"Zwangsöffnender Positionsschalter", "Codierter Sicherheitssensor", "Anti-Tamper-Masssnahmen"}),
+ IsBuiltin: true,
+ TenantID: nil,
+ CreatedAt: now,
+ },
+ {
+ ID: hazardUUID("safety_function_failure", 3),
+ Category: "safety_function_failure",
+ Name: "Safe Speed Monitoring fehlt",
+ Description: "Beim Einrichten im reduzierten Betrieb fehlt eine unabhaengige Geschwindigkeitsueberwachung, so dass der Bediener nicht ausreichend geschuetzt ist.",
+ DefaultSeverity: 5,
+ DefaultProbability: 2,
+ ApplicableComponentTypes: []string{"controller", "software"},
+ RegulationReferences: []string{"Maschinenverordnung 2023/1230", "IEC 62061", "ISO 13849"},
+ SuggestedMitigations: mustMarshalJSON([]string{"Sicherheitsumrichter mit SLS", "Unabhaengige Drehzahlmessung", "SIL-2-Geschwindigkeitsueberwachung"}),
+ IsBuiltin: true,
+ TenantID: nil,
+ CreatedAt: now,
+ },
+ {
+ ID: hazardUUID("safety_function_failure", 4),
+ Category: "safety_function_failure",
+ Name: "STO-Funktion (Safe Torque Off) Fehler",
+ Description: "Die STO-Sicherheitsfunktion schaltet den Antriebsmoment nicht ab, obwohl die Funktion aktiviert wurde, z.B. durch Fehler im Sicherheits-SPS-Ausgang.",
+ DefaultSeverity: 5,
+ DefaultProbability: 2,
+ ApplicableComponentTypes: []string{"actuator", "controller"},
+ RegulationReferences: []string{"IEC 61800-5-2", "Maschinenverordnung 2023/1230", "IEC 62061"},
+ SuggestedMitigations: mustMarshalJSON([]string{"STO-Pruefung bei Inbetriebnahme", "Pruefzyklus im Betrieb", "Zertifizierter Sicherheitsumrichter"}),
+ IsBuiltin: true,
+ TenantID: nil,
+ CreatedAt: now,
+ },
+ {
+ ID: hazardUUID("safety_function_failure", 5),
+ Category: "safety_function_failure",
+ Name: "Muting-Missbrauch bei Lichtvorhang",
+ Description: "Die Muting-Funktion des Lichtvorhangs wird durch Fehler oder Manipulation zu lange oder unkontrolliert aktiviert, was den Schutz aufhebt.",
+ DefaultSeverity: 5,
+ DefaultProbability: 2,
+ ApplicableComponentTypes: []string{"sensor", "controller"},
+ RegulationReferences: []string{"IEC 61496-3", "Maschinenverordnung 2023/1230"},
+ SuggestedMitigations: mustMarshalJSON([]string{"Zeitbegrenztes Muting", "Muting-Lampe und Alarm", "Protokollierung der Muting-Ereignisse"}),
+ IsBuiltin: true,
+ TenantID: nil,
+ CreatedAt: now,
+ },
+ {
+ ID: hazardUUID("safety_function_failure", 6),
+ Category: "safety_function_failure",
+ Name: "Zweihand-Taster durch Gegenstand ueberbrueckt",
+ Description: "Die Zweihand-Betaetigungseinrichtung wird durch ein eingeklemmtes Objekt permanent aktiviert, was den Bediener aus dem Schutzkonzept loest.",
+ DefaultSeverity: 5,
+ DefaultProbability: 2,
+ ApplicableComponentTypes: []string{"hmi", "controller"},
+ RegulationReferences: []string{"ISO 13851", "Maschinenverordnung 2023/1230", "ISO 13849"},
+ SuggestedMitigations: mustMarshalJSON([]string{"Anti-Tie-Down-Pruefung", "Typ-III-Zweihand-Taster", "Regelmaessige Funktionskontrolle"}),
+ IsBuiltin: true,
+ TenantID: nil,
+ CreatedAt: now,
+ },
+ {
+ ID: hazardUUID("safety_function_failure", 7),
+ Category: "safety_function_failure",
+ Name: "Sicherheitsrelais-Ausfall ohne Erkennung",
+ Description: "Ein Sicherheitsrelais versagt unentdeckt (z.B. verklebte Kontakte), sodass der Sicherheitskreis nicht mehr auftrennt.",
+ DefaultSeverity: 5,
+ DefaultProbability: 2,
+ ApplicableComponentTypes: []string{"controller"},
+ RegulationReferences: []string{"ISO 13849", "IEC 62061"},
+ SuggestedMitigations: mustMarshalJSON([]string{"Selbstueberwachung (zwangsgefuehrt)", "Regelmaessiger Testlauf", "Redundantes Relais"}),
+ IsBuiltin: true,
+ TenantID: nil,
+ CreatedAt: now,
+ },
+ {
+ ID: hazardUUID("safety_function_failure", 8),
+ Category: "safety_function_failure",
+ Name: "Logic-Solver-Fehler in Sicherheits-SPS",
+ Description: "Die Sicherheitssteuerung (Safety-SPS) fuehrt sicherheitsrelevante Logik fehlerhaft aus, z.B. durch Speicherfehler oder Prozessorfehler.",
+ DefaultSeverity: 5,
+ DefaultProbability: 1,
+ ApplicableComponentTypes: []string{"controller", "software"},
+ RegulationReferences: []string{"IEC 61511", "IEC 61508", "ISO 13849"},
+ SuggestedMitigations: mustMarshalJSON([]string{"SIL-zertifizierte SPS", "Watchdog", "Selbsttest-Routinen (BIST)"}),
+ IsBuiltin: true,
+ TenantID: nil,
+ CreatedAt: now,
+ },
+
+ // ====================================================================
+ // Category: environmental_hazard (5 entries)
+ // ====================================================================
+ {
+ ID: hazardUUID("environmental_hazard", 1),
+ Category: "environmental_hazard",
+ Name: "Ausfall durch hohe Umgebungstemperatur",
+ Description: "Hohe Umgebungstemperaturen ueberschreiten die spezifizierten Grenzwerte der Elektronik oder Aktorik und fuehren zu Fehlfunktionen.",
+ DefaultSeverity: 4,
+ DefaultProbability: 3,
+ ApplicableComponentTypes: []string{"controller", "sensor"},
+ RegulationReferences: []string{"Maschinenverordnung 2023/1230", "IEC 60068-2"},
+ SuggestedMitigations: mustMarshalJSON([]string{"Betriebstemperatur-Spezifikation einhalten", "Klimaanlagensystem", "Temperatursensor + Abschaltung"}),
+ IsBuiltin: true,
+ TenantID: nil,
+ CreatedAt: now,
+ },
+ {
+ ID: hazardUUID("environmental_hazard", 2),
+ Category: "environmental_hazard",
+ Name: "Ausfall bei Tieftemperatur",
+ Description: "Sehr tiefe Temperaturen reduzieren die Viskositaet von Hydraulikfluessigkeiten, beeinflussen Elektronik und fuehren zu mechanischen Ausfaellen.",
+ DefaultSeverity: 4,
+ DefaultProbability: 3,
+ ApplicableComponentTypes: []string{"actuator", "controller"},
+ RegulationReferences: []string{"Maschinenverordnung 2023/1230", "IEC 60068-2"},
+ SuggestedMitigations: mustMarshalJSON([]string{"Tieftemperatur-spezifizierte Komponenten", "Heizung im Schaltschrank", "Anlaeufroutine bei Kaeltestart"}),
+ IsBuiltin: true,
+ TenantID: nil,
+ CreatedAt: now,
+ },
+ {
+ ID: hazardUUID("environmental_hazard", 3),
+ Category: "environmental_hazard",
+ Name: "Korrosion durch Feuchtigkeit",
+ Description: "Hohe Luftfeuchtigkeit oder Kondenswasser fuehrt zur Korrosion von Kontakten und Leiterbahnen, was zu Ausfaellen und Isolationsfehlern fuehrt.",
+ DefaultSeverity: 3,
+ DefaultProbability: 4,
+ ApplicableComponentTypes: []string{"controller", "sensor"},
+ RegulationReferences: []string{"Maschinenverordnung 2023/1230", "IEC 60529"},
+ SuggestedMitigations: mustMarshalJSON([]string{"IP-Schutz entsprechend der Umgebung", "Belueftung mit Filter", "Regelmaessige Inspektion"}),
+ IsBuiltin: true,
+ TenantID: nil,
+ CreatedAt: now,
+ },
+ {
+ ID: hazardUUID("environmental_hazard", 4),
+ Category: "environmental_hazard",
+ Name: "Fehlfunktion durch Vibrationen",
+ Description: "Mechanische Vibrationen lockern Verbindungen, schuetteln Kontakte auf oder beschaedigen Loetpunkte in Elektronikbaugruppen.",
+ DefaultSeverity: 4,
+ DefaultProbability: 3,
+ ApplicableComponentTypes: []string{"controller", "sensor"},
+ RegulationReferences: []string{"Maschinenverordnung 2023/1230", "IEC 60068-2-6"},
+ SuggestedMitigations: mustMarshalJSON([]string{"Vibrationsdaempfung", "Vergossene Elektronik", "Regelmaessige Verbindungskontrolle"}),
+ IsBuiltin: true,
+ TenantID: nil,
+ CreatedAt: now,
+ },
+ {
+ ID: hazardUUID("environmental_hazard", 5),
+ Category: "environmental_hazard",
+ Name: "Kontamination durch Staub oder Fluessigkeiten",
+ Description: "Staub, Metallspaeene oder Kuehlmittel gelangen in das Gehaeuseinnere und fuehren zu Kurzschluessen, Isolationsfehlern oder Kuehlproblemen.",
+ DefaultSeverity: 3,
+ DefaultProbability: 4,
+ ApplicableComponentTypes: []string{"controller", "hmi"},
+ RegulationReferences: []string{"Maschinenverordnung 2023/1230", "IEC 60529"},
+ SuggestedMitigations: mustMarshalJSON([]string{"Hohe IP-Schutzklasse", "Dichtungen regelmaessig pruefen", "Ueberdruck im Schaltschrank"}),
+ IsBuiltin: true,
+ TenantID: nil,
+ CreatedAt: now,
+ },
+
+ // ====================================================================
+ // Category: maintenance_hazard (6 entries)
+ // ====================================================================
+ {
+ ID: hazardUUID("maintenance_hazard", 1),
+ Category: "maintenance_hazard",
+ Name: "Wartung ohne LOTO-Prozedur",
+ Description: "Wartungsarbeiten werden ohne korrekte Lockout/Tagout-Prozedur durchgefuehrt, sodass die Maschine waehrend der Arbeit anlaufen kann.",
+ DefaultSeverity: 5,
+ DefaultProbability: 3,
+ ApplicableComponentTypes: []string{"controller", "software"},
+ RegulationReferences: []string{"Maschinenverordnung 2023/1230 Anhang I §1.6.3"},
+ SuggestedMitigations: mustMarshalJSON([]string{"LOTO-Funktion in Software", "Schulung", "Prozedur im Betriebshandbuch"}),
+ IsBuiltin: true,
+ TenantID: nil,
+ CreatedAt: now,
+ },
+ {
+ ID: hazardUUID("maintenance_hazard", 2),
+ Category: "maintenance_hazard",
+ Name: "Fehlende LOTO-Funktion in Software",
+ Description: "Die Steuerungssoftware bietet keine Moeglichkeit, die Maschine fuer Wartungsarbeiten sicher zu sperren und zu verriegeln.",
+ DefaultSeverity: 5,
+ DefaultProbability: 2,
+ ApplicableComponentTypes: []string{"software", "hmi"},
+ RegulationReferences: []string{"Maschinenverordnung 2023/1230 Anhang I §1.6.3"},
+ SuggestedMitigations: mustMarshalJSON([]string{"Software-LOTO implementieren", "Wartungsmodus mit Schluessel", "Energiesperrfunktion"}),
+ IsBuiltin: true,
+ TenantID: nil,
+ CreatedAt: now,
+ },
+ {
+ ID: hazardUUID("maintenance_hazard", 3),
+ Category: "maintenance_hazard",
+ Name: "Wartung bei laufender Maschine",
+ Description: "Wartungsarbeiten werden an betriebener Maschine durchgefuehrt, weil kein erzwungener Wartungsmodus vorhanden ist.",
+ DefaultSeverity: 5,
+ DefaultProbability: 2,
+ ApplicableComponentTypes: []string{"software", "controller"},
+ RegulationReferences: []string{"Maschinenverordnung 2023/1230", "ISO 13849"},
+ SuggestedMitigations: mustMarshalJSON([]string{"Erzwungenes Abschalten fuer Wartungsmodus", "Schluesselschalter", "Schutzmassnahmen im Wartungsmodus"}),
+ IsBuiltin: true,
+ TenantID: nil,
+ CreatedAt: now,
+ },
+ {
+ ID: hazardUUID("maintenance_hazard", 4),
+ Category: "maintenance_hazard",
+ Name: "Wartungs-Tool ohne Zugangskontrolle",
+ Description: "Ein Diagnose- oder Wartungswerkzeug ist ohne Authentifizierung zugaenglich und ermoeglicht die unbeaufsichtigte Aenderung von Sicherheitsparametern.",
+ DefaultSeverity: 4,
+ DefaultProbability: 2,
+ ApplicableComponentTypes: []string{"software", "hmi"},
+ RegulationReferences: []string{"IEC 62443", "CRA"},
+ SuggestedMitigations: mustMarshalJSON([]string{"Authentifizierung fuer Wartungs-Tools", "Rollenkonzept", "Audit-Log fuer Wartungszugriffe"}),
+ IsBuiltin: true,
+ TenantID: nil,
+ CreatedAt: now,
+ },
+ {
+ ID: hazardUUID("maintenance_hazard", 5),
+ Category: "maintenance_hazard",
+ Name: "Unsichere Demontage gefaehrlicher Baugruppen",
+ Description: "Die Betriebsanleitung beschreibt nicht, wie gefaehrliche Baugruppen (z.B. Hochvolt, gespeicherte Energie) sicher demontiert werden.",
+ DefaultSeverity: 5,
+ DefaultProbability: 2,
+ ApplicableComponentTypes: []string{"other"},
+ RegulationReferences: []string{"Maschinenverordnung 2023/1230 Anhang I §1.7.4"},
+ SuggestedMitigations: mustMarshalJSON([]string{"Detaillierte Demontageanleitung", "Warnhinweise an Geraet", "Schulung des Wartungspersonals"}),
+ IsBuiltin: true,
+ TenantID: nil,
+ CreatedAt: now,
+ },
+ {
+ ID: hazardUUID("maintenance_hazard", 6),
+ Category: "maintenance_hazard",
+ Name: "Wiederanlauf nach Wartung ohne Freigabeprozedur",
+ Description: "Nach Wartungsarbeiten wird die Maschine ohne formelle Freigabeprozedur wieder in Betrieb genommen, was zu Verletzungen bei noch anwesendem Personal fuehren kann.",
+ DefaultSeverity: 5,
+ DefaultProbability: 2,
+ ApplicableComponentTypes: []string{"software", "hmi"},
+ RegulationReferences: []string{"Maschinenverordnung 2023/1230 Anhang I §1.6.3", "ISO 13849"},
+ SuggestedMitigations: mustMarshalJSON([]string{"Software-Wiederanlauf-Freigabe", "Gefahrenbereich-Pruefung vor Anlauf", "Akustisches Warnsignal vor Anlauf"}),
+ IsBuiltin: true,
+ TenantID: nil,
+ CreatedAt: now,
+ },
+ }
+}
diff --git a/ai-compliance-sdk/internal/iace/hazard_library_software_hmi.go b/ai-compliance-sdk/internal/iace/hazard_library_software_hmi.go
new file mode 100644
index 0000000..e6db2b9
--- /dev/null
+++ b/ai-compliance-sdk/internal/iace/hazard_library_software_hmi.go
@@ -0,0 +1,578 @@
+package iace
+
+import "time"
+
+// builtinHazardsSoftwareHMI returns extended hazard library entries covering
+// software faults, HMI errors, configuration errors, logging/audit failures,
+// and integration errors.
+func builtinHazardsSoftwareHMI() []HazardLibraryEntry {
+ now := time.Now()
+
+ return []HazardLibraryEntry{
+ // ====================================================================
+ // Category: software_fault (10 entries)
+ // ====================================================================
+ {
+ ID: hazardUUID("software_fault", 1),
+ Category: "software_fault",
+ Name: "Race Condition in Sicherheitsfunktion",
+ Description: "Zwei Tasks greifen ohne Synchronisation auf gemeinsame Ressourcen zu, was zu unvorhersehbarem Verhalten in sicherheitsrelevanten Funktionen fuehren kann.",
+ DefaultSeverity: 5,
+ DefaultProbability: 3,
+ ApplicableComponentTypes: []string{"software", "firmware"},
+ RegulationReferences: []string{"EU 2023/1230 Anhang I §1.2", "IEC 62304", "IEC 61508"},
+ SuggestedMitigations: mustMarshalJSON([]string{"Mutex/Semaphor", "RTOS-Task-Prioritaeten", "WCET-Analyse"}),
+ IsBuiltin: true,
+ TenantID: nil,
+ CreatedAt: now,
+ },
+ {
+ ID: hazardUUID("software_fault", 2),
+ Category: "software_fault",
+ Name: "Stack Overflow in Echtzeit-Task",
+ Description: "Ein rekursiver Aufruf oder grosse lokale Variablen fuehren zum Stack-Ueberlauf, was Safety-Tasks zum Absturz bringt.",
+ DefaultSeverity: 4,
+ DefaultProbability: 3,
+ ApplicableComponentTypes: []string{"software", "firmware"},
+ RegulationReferences: []string{"IEC 62304", "IEC 61508"},
+ SuggestedMitigations: mustMarshalJSON([]string{"Stack-Groessen-Analyse", "Stack-Guard", "Statische Code-Analyse"}),
+ IsBuiltin: true,
+ TenantID: nil,
+ CreatedAt: now,
+ },
+ {
+ ID: hazardUUID("software_fault", 3),
+ Category: "software_fault",
+ Name: "Integer Overflow in Sicherheitsberechnung",
+ Description: "Arithmetischer Ueberlauf bei der Berechnung sicherheitskritischer Grenzwerte fuehrt zu falschen Ergebnissen und unkontrolliertem Verhalten.",
+ DefaultSeverity: 5,
+ DefaultProbability: 2,
+ ApplicableComponentTypes: []string{"software", "firmware"},
+ RegulationReferences: []string{"IEC 62304", "MISRA-C", "IEC 61508"},
+ SuggestedMitigations: mustMarshalJSON([]string{"Datentyp-Pruefung", "Overflow-Detection", "MISRA-C-Analyse"}),
+ IsBuiltin: true,
+ TenantID: nil,
+ CreatedAt: now,
+ },
+ {
+ ID: hazardUUID("software_fault", 4),
+ Category: "software_fault",
+ Name: "Deadlock zwischen Safety-Tasks",
+ Description: "Gegenseitige Sperrung von Tasks durch zyklische Ressourcenabhaengigkeiten verhindert die Ausfuehrung sicherheitsrelevanter Funktionen.",
+ DefaultSeverity: 4,
+ DefaultProbability: 3,
+ ApplicableComponentTypes: []string{"software", "firmware"},
+ RegulationReferences: []string{"IEC 62304", "IEC 61508"},
+ SuggestedMitigations: mustMarshalJSON([]string{"Ressourcen-Hierarchie", "Watchdog", "Deadlock-Analyse"}),
+ IsBuiltin: true,
+ TenantID: nil,
+ CreatedAt: now,
+ },
+ {
+ ID: hazardUUID("software_fault", 5),
+ Category: "software_fault",
+ Name: "Memory Leak im Langzeitbetrieb",
+ Description: "Nicht freigegebener Heap-Speicher akkumuliert sich ueber Zeit, bis das System abstuerzt und Sicherheitsfunktionen nicht mehr verfuegbar sind.",
+ DefaultSeverity: 3,
+ DefaultProbability: 4,
+ ApplicableComponentTypes: []string{"software"},
+ RegulationReferences: []string{"IEC 62304", "IEC 61508"},
+ SuggestedMitigations: mustMarshalJSON([]string{"Memory-Profiling", "Valgrind", "Statisches Speichermanagement"}),
+ IsBuiltin: true,
+ TenantID: nil,
+ CreatedAt: now,
+ },
+ {
+ ID: hazardUUID("software_fault", 6),
+ Category: "software_fault",
+ Name: "Null-Pointer-Dereferenz in Safety-Code",
+ Description: "Zugriff auf einen Null-Zeiger fuehrt zu einem undefinierten Systemzustand oder Absturz des sicherheitsrelevanten Software-Moduls.",
+ DefaultSeverity: 4,
+ DefaultProbability: 3,
+ ApplicableComponentTypes: []string{"software", "firmware"},
+ RegulationReferences: []string{"IEC 62304", "MISRA-C"},
+ SuggestedMitigations: mustMarshalJSON([]string{"Null-Check vor Zugriff", "Statische Analyse", "Defensiv-Programmierung"}),
+ IsBuiltin: true,
+ TenantID: nil,
+ CreatedAt: now,
+ },
+ {
+ ID: hazardUUID("software_fault", 7),
+ Category: "software_fault",
+ Name: "Unbehandelte Ausnahme in Safety-Code",
+ Description: "Eine nicht abgefangene Ausnahme bricht die Ausfuehrung des sicherheitsrelevanten Codes ab und hinterlaesst das System in einem undefinierten Zustand.",
+ DefaultSeverity: 5,
+ DefaultProbability: 2,
+ ApplicableComponentTypes: []string{"software"},
+ RegulationReferences: []string{"IEC 62304", "IEC 61508"},
+ SuggestedMitigations: mustMarshalJSON([]string{"Globaler Exception-Handler", "Exception-Safety-Analyse", "Fail-Safe-Rueckfall"}),
+ IsBuiltin: true,
+ TenantID: nil,
+ CreatedAt: now,
+ },
+ {
+ ID: hazardUUID("software_fault", 8),
+ Category: "software_fault",
+ Name: "Korrupte Konfigurationsdaten",
+ Description: "Beschaedigte oder unvollstaendige Konfigurationsdaten werden ohne Validierung geladen und fuehren zu falschem Systemverhalten.",
+ DefaultSeverity: 4,
+ DefaultProbability: 3,
+ ApplicableComponentTypes: []string{"software", "firmware"},
+ RegulationReferences: []string{"IEC 62304", "Maschinenverordnung 2023/1230"},
+ SuggestedMitigations: mustMarshalJSON([]string{"Konfig-Validierung", "CRC-Pruefung", "Fallback-Konfiguration"}),
+ IsBuiltin: true,
+ TenantID: nil,
+ CreatedAt: now,
+ },
+ {
+ ID: hazardUUID("software_fault", 9),
+ Category: "software_fault",
+ Name: "Division durch Null in Regelkreis",
+ Description: "Ein Divisor im sicherheitsrelevanten Regelkreis erreicht den Wert Null, was zu einem Laufzeitfehler oder undefiniertem Ergebnis fuehrt.",
+ DefaultSeverity: 5,
+ DefaultProbability: 2,
+ ApplicableComponentTypes: []string{"software", "firmware"},
+ RegulationReferences: []string{"IEC 62304", "IEC 61508", "MISRA-C"},
+ SuggestedMitigations: mustMarshalJSON([]string{"Vorbedingungspruefung", "Statische Analyse", "Defensiv-Programmierung"}),
+ IsBuiltin: true,
+ TenantID: nil,
+ CreatedAt: now,
+ },
+ {
+ ID: hazardUUID("software_fault", 10),
+ Category: "software_fault",
+ Name: "Falscher Safety-Parameter durch Software-Bug",
+ Description: "Ein Software-Fehler setzt einen sicherheitsrelevanten Parameter auf einen falschen Wert, ohne dass eine Plausibilitaetspruefung dies erkennt.",
+ DefaultSeverity: 5,
+ DefaultProbability: 2,
+ ApplicableComponentTypes: []string{"software", "firmware"},
+ RegulationReferences: []string{"IEC 62304", "IEC 61508", "Maschinenverordnung 2023/1230"},
+ SuggestedMitigations: mustMarshalJSON([]string{"Parametervalidierung", "Redundante Speicherung", "Diversitaere Pruefung"}),
+ IsBuiltin: true,
+ TenantID: nil,
+ CreatedAt: now,
+ },
+
+ // ====================================================================
+ // Category: hmi_error (8 entries)
+ // ====================================================================
+ {
+ ID: hazardUUID("hmi_error", 1),
+ Category: "hmi_error",
+ Name: "Falsche Einheitendarstellung",
+ Description: "Das HMI zeigt Werte in einer falschen Masseinheit an (z.B. mm statt inch), was zu Fehlbedienung und Maschinenfehlern fuehren kann.",
+ DefaultSeverity: 4,
+ DefaultProbability: 3,
+ ApplicableComponentTypes: []string{"hmi", "software"},
+ RegulationReferences: []string{"Maschinenverordnung 2023/1230 Anhang III", "EN ISO 9241"},
+ SuggestedMitigations: mustMarshalJSON([]string{"Einheiten-Label im UI", "Lokalisierungstests", "Einheiten-Konvertierungspruefung"}),
+ IsBuiltin: true,
+ TenantID: nil,
+ CreatedAt: now,
+ },
+ {
+ ID: hazardUUID("hmi_error", 2),
+ Category: "hmi_error",
+ Name: "Fehlender oder stummer Sicherheitsalarm",
+ Description: "Ein kritisches Sicherheitsereignis wird dem Bediener nicht oder nicht rechtzeitig angezeigt, weil die Alarmfunktion deaktiviert oder fehlerhaft ist.",
+ DefaultSeverity: 5,
+ DefaultProbability: 2,
+ ApplicableComponentTypes: []string{"hmi", "software"},
+ RegulationReferences: []string{"Maschinenverordnung 2023/1230", "ISO 13849", "EN ISO 9241"},
+ SuggestedMitigations: mustMarshalJSON([]string{"Alarmtest im Rahmen der Inbetriebnahme", "Akustischer Backup-Alarm", "Alarmverwaltungssystem"}),
+ IsBuiltin: true,
+ TenantID: nil,
+ CreatedAt: now,
+ },
+ {
+ ID: hazardUUID("hmi_error", 3),
+ Category: "hmi_error",
+ Name: "Sprachfehler in Bedienoberflaeche",
+ Description: "Fehlerhafte oder mehrdeutige Bezeichnungen in der Benutzersprache fuehren zu Fehlbedienung sicherheitsrelevanter Funktionen.",
+ DefaultSeverity: 3,
+ DefaultProbability: 3,
+ ApplicableComponentTypes: []string{"hmi"},
+ RegulationReferences: []string{"Maschinenverordnung 2023/1230 Anhang III"},
+ SuggestedMitigations: mustMarshalJSON([]string{"Usability-Test", "Lokalisierungs-Review", "Mehrsprachige Dokumentation"}),
+ IsBuiltin: true,
+ TenantID: nil,
+ CreatedAt: now,
+ },
+ {
+ ID: hazardUUID("hmi_error", 4),
+ Category: "hmi_error",
+ Name: "Fehlende Eingabevalidierung im HMI",
+ Description: "Das HMI akzeptiert ausserhalb des gueltigen Bereichs liegende Eingaben ohne Warnung und leitet sie an die Steuerung weiter.",
+ DefaultSeverity: 4,
+ DefaultProbability: 3,
+ ApplicableComponentTypes: []string{"hmi", "software"},
+ RegulationReferences: []string{"Maschinenverordnung 2023/1230", "IEC 62304"},
+ SuggestedMitigations: mustMarshalJSON([]string{"Grenzwertpruefung", "Eingabemaske mit Bereichen", "Warnung bei Grenzwertnaehe"}),
+ IsBuiltin: true,
+ TenantID: nil,
+ CreatedAt: now,
+ },
+ {
+ ID: hazardUUID("hmi_error", 5),
+ Category: "hmi_error",
+ Name: "Defekter Statusindikator",
+ Description: "Ein LED, Anzeigeelement oder Softwaresymbol zeigt einen falschen Systemstatus an und verleitet den Bediener zu falschen Annahmen.",
+ DefaultSeverity: 4,
+ DefaultProbability: 3,
+ ApplicableComponentTypes: []string{"hmi"},
+ RegulationReferences: []string{"Maschinenverordnung 2023/1230 Anhang III"},
+ SuggestedMitigations: mustMarshalJSON([]string{"Regelmaessige HMI-Tests", "Selbsttest beim Einschalten", "Redundante Statusanzeige"}),
+ IsBuiltin: true,
+ TenantID: nil,
+ CreatedAt: now,
+ },
+ {
+ ID: hazardUUID("hmi_error", 6),
+ Category: "hmi_error",
+ Name: "Quittierung ohne Ursachenbehebung",
+ Description: "Der Bediener kann einen Sicherheitsalarm quittieren, ohne die zugrundeliegende Ursache behoben zu haben, was das Risiko wiederkehrender Ereignisse erhoet.",
+ DefaultSeverity: 4,
+ DefaultProbability: 3,
+ ApplicableComponentTypes: []string{"hmi", "software"},
+ RegulationReferences: []string{"Maschinenverordnung 2023/1230", "ISO 13849"},
+ SuggestedMitigations: mustMarshalJSON([]string{"Ursachen-Checkliste vor Quittierung", "Pflicht-Ursachen-Eingabe", "Audit-Log der Quittierungen"}),
+ IsBuiltin: true,
+ TenantID: nil,
+ CreatedAt: now,
+ },
+ {
+ ID: hazardUUID("hmi_error", 7),
+ Category: "hmi_error",
+ Name: "Veraltete Anzeige durch Caching-Fehler",
+ Description: "Die HMI-Anzeige wird nicht aktualisiert und zeigt veraltete Sensorwerte oder Zustaende an, was zu Fehlentscheidungen fuehrt.",
+ DefaultSeverity: 4,
+ DefaultProbability: 3,
+ ApplicableComponentTypes: []string{"hmi", "software"},
+ RegulationReferences: []string{"Maschinenverordnung 2023/1230 Anhang III"},
+ SuggestedMitigations: mustMarshalJSON([]string{"Timestamp-Anzeige", "Refresh-Watchdog", "Verbindungsstatus-Indikator"}),
+ IsBuiltin: true,
+ TenantID: nil,
+ CreatedAt: now,
+ },
+ {
+ ID: hazardUUID("hmi_error", 8),
+ Category: "hmi_error",
+ Name: "Fehlende Betriebsart-Kennzeichnung",
+ Description: "Die aktive Betriebsart (Automatik, Einrichten, Wartung) ist im HMI nicht eindeutig sichtbar, was zu unerwarteten Maschinenbewegungen fuehren kann.",
+ DefaultSeverity: 4,
+ DefaultProbability: 3,
+ ApplicableComponentTypes: []string{"hmi", "software"},
+ RegulationReferences: []string{"Maschinenverordnung 2023/1230", "ISO 13849"},
+ SuggestedMitigations: mustMarshalJSON([]string{"Dauerhafte Betriebsart-Anzeige", "Farbliche Kennzeichnung", "Bestaetigung bei Modewechsel"}),
+ IsBuiltin: true,
+ TenantID: nil,
+ CreatedAt: now,
+ },
+
+ // ====================================================================
+ // Category: configuration_error (8 entries)
+ // ====================================================================
+ {
+ ID: hazardUUID("configuration_error", 1),
+ Category: "configuration_error",
+ Name: "Falscher Safety-Parameter bei Inbetriebnahme",
+ Description: "Beim Einrichten werden sicherheitsrelevante Parameter (z.B. Maximalgeschwindigkeit, Abschaltgrenzen) falsch konfiguriert und nicht verifiziert.",
+ DefaultSeverity: 5,
+ DefaultProbability: 3,
+ ApplicableComponentTypes: []string{"software", "firmware", "controller"},
+ RegulationReferences: []string{"Maschinenverordnung 2023/1230", "ISO 13849", "IEC 62061"},
+ SuggestedMitigations: mustMarshalJSON([]string{"Parameterpruefung nach Inbetriebnahme", "4-Augen-Prinzip", "Parameterprotokoll in technischer Akte"}),
+ IsBuiltin: true,
+ TenantID: nil,
+ CreatedAt: now,
+ },
+ {
+ ID: hazardUUID("configuration_error", 2),
+ Category: "configuration_error",
+ Name: "Factory Reset loescht Sicherheitskonfiguration",
+ Description: "Ein Factory Reset setzt alle Parameter auf Werkseinstellungen zurueck, einschliesslich sicherheitsrelevanter Konfigurationen, ohne Warnung.",
+ DefaultSeverity: 5,
+ DefaultProbability: 2,
+ ApplicableComponentTypes: []string{"firmware", "software"},
+ RegulationReferences: []string{"IEC 62304", "CRA", "Maschinenverordnung 2023/1230"},
+ SuggestedMitigations: mustMarshalJSON([]string{"Separate Safety-Partition", "Bestaetigung vor Reset", "Safety-Config vor Reset sichern"}),
+ IsBuiltin: true,
+ TenantID: nil,
+ CreatedAt: now,
+ },
+ {
+ ID: hazardUUID("configuration_error", 3),
+ Category: "configuration_error",
+ Name: "Fehlerhafte Parameter-Migration bei Update",
+ Description: "Beim Software-Update werden vorhandene Konfigurationsparameter nicht korrekt in das neue Format migriert, was zu falschen Systemeinstellungen fuehrt.",
+ DefaultSeverity: 5,
+ DefaultProbability: 2,
+ ApplicableComponentTypes: []string{"software", "firmware"},
+ RegulationReferences: []string{"IEC 62304", "CRA"},
+ SuggestedMitigations: mustMarshalJSON([]string{"Migrations-Skript-Tests", "Konfig-Backup vor Update", "Post-Update-Verifikation"}),
+ IsBuiltin: true,
+ TenantID: nil,
+ CreatedAt: now,
+ },
+ {
+ ID: hazardUUID("configuration_error", 4),
+ Category: "configuration_error",
+ Name: "Konflikthafte redundante Einstellungen",
+ Description: "Widersprüchliche Parameter in verschiedenen Konfigurationsdateien oder -ebenen fuehren zu unvorhersehbarem Systemverhalten.",
+ DefaultSeverity: 4,
+ DefaultProbability: 3,
+ ApplicableComponentTypes: []string{"software", "firmware"},
+ RegulationReferences: []string{"IEC 62304", "Maschinenverordnung 2023/1230"},
+ SuggestedMitigations: mustMarshalJSON([]string{"Konfig-Validierung beim Start", "Einzelne Quelle fuer Safety-Params", "Konsistenzpruefung"}),
+ IsBuiltin: true,
+ TenantID: nil,
+ CreatedAt: now,
+ },
+ {
+ ID: hazardUUID("configuration_error", 5),
+ Category: "configuration_error",
+ Name: "Hard-coded Credentials in Konfiguration",
+ Description: "Passwörter oder Schluessel sind fest im Code oder in Konfigurationsdateien hinterlegt und koennen nicht geaendert werden.",
+ DefaultSeverity: 4,
+ DefaultProbability: 2,
+ ApplicableComponentTypes: []string{"software", "firmware"},
+ RegulationReferences: []string{"CRA", "IEC 62443"},
+ SuggestedMitigations: mustMarshalJSON([]string{"Secrets-Management", "Kein Hard-Coding", "Credential-Scan im CI"}),
+ IsBuiltin: true,
+ TenantID: nil,
+ CreatedAt: now,
+ },
+ {
+ ID: hazardUUID("configuration_error", 6),
+ Category: "configuration_error",
+ Name: "Debug-Modus in Produktionsumgebung aktiv",
+ Description: "Debug-Schnittstellen oder erhoehte Logging-Level sind in der Produktionsumgebung aktiv und ermoeglichen Angreifern Zugang zu sensiblen Systeminfos.",
+ DefaultSeverity: 4,
+ DefaultProbability: 2,
+ ApplicableComponentTypes: []string{"software", "firmware"},
+ RegulationReferences: []string{"CRA", "IEC 62443"},
+ SuggestedMitigations: mustMarshalJSON([]string{"Build-Konfiguration pruefe Debug-Flag", "Produktions-Checkliste", "Debug-Port-Deaktivierung"}),
+ IsBuiltin: true,
+ TenantID: nil,
+ CreatedAt: now,
+ },
+ {
+ ID: hazardUUID("configuration_error", 7),
+ Category: "configuration_error",
+ Name: "Out-of-Bounds-Eingabe ohne Validierung",
+ Description: "Nutzereingaben oder Schnittstellendaten werden ohne Bereichspruefung in sicherheitsrelevante Parameter uebernommen.",
+ DefaultSeverity: 4,
+ DefaultProbability: 3,
+ ApplicableComponentTypes: []string{"software", "hmi"},
+ RegulationReferences: []string{"IEC 62304", "Maschinenverordnung 2023/1230"},
+ SuggestedMitigations: mustMarshalJSON([]string{"Eingabevalidierung", "Bereichsgrenzen definieren", "Sanity-Check"}),
+ IsBuiltin: true,
+ TenantID: nil,
+ CreatedAt: now,
+ },
+ {
+ ID: hazardUUID("configuration_error", 8),
+ Category: "configuration_error",
+ Name: "Konfigurationsdatei nicht schreibgeschuetzt",
+ Description: "Sicherheitsrelevante Konfigurationsdateien koennen von unautorisierten Nutzern oder Prozessen veraendert werden.",
+ DefaultSeverity: 4,
+ DefaultProbability: 3,
+ ApplicableComponentTypes: []string{"software", "firmware"},
+ RegulationReferences: []string{"IEC 62443", "CRA"},
+ SuggestedMitigations: mustMarshalJSON([]string{"Dateisystem-Berechtigungen", "Code-Signing fuer Konfig", "Integritaetspruefung"}),
+ IsBuiltin: true,
+ TenantID: nil,
+ CreatedAt: now,
+ },
+
+ // ====================================================================
+ // Category: logging_audit_failure (5 entries)
+ // ====================================================================
+ {
+ ID: hazardUUID("logging_audit_failure", 1),
+ Category: "logging_audit_failure",
+ Name: "Safety-Events nicht protokolliert",
+ Description: "Sicherheitsrelevante Ereignisse (Alarme, Not-Halt-Betaetigungen, Fehlerzustaende) werden nicht in ein Protokoll geschrieben.",
+ DefaultSeverity: 4,
+ DefaultProbability: 3,
+ ApplicableComponentTypes: []string{"software", "controller"},
+ RegulationReferences: []string{"Maschinenverordnung 2023/1230", "IEC 62443", "CRA"},
+ SuggestedMitigations: mustMarshalJSON([]string{"Pflicht-Logging Safety-Events", "Unveraenderliches Audit-Log", "Log-Integritaetspruefung"}),
+ IsBuiltin: true,
+ TenantID: nil,
+ CreatedAt: now,
+ },
+ {
+ ID: hazardUUID("logging_audit_failure", 2),
+ Category: "logging_audit_failure",
+ Name: "Log-Manipulation moeglich",
+ Description: "Authentifizierte Benutzer oder Angreifer koennen Protokolleintraege aendern oder loeschen und so Beweise fuer Sicherheitsvorfaelle vernichten.",
+ DefaultSeverity: 4,
+ DefaultProbability: 2,
+ ApplicableComponentTypes: []string{"software"},
+ RegulationReferences: []string{"CRA", "IEC 62443"},
+ SuggestedMitigations: mustMarshalJSON([]string{"Write-Once-Speicher", "Kryptografische Signaturen", "Externes Log-Management"}),
+ IsBuiltin: true,
+ TenantID: nil,
+ CreatedAt: now,
+ },
+ {
+ ID: hazardUUID("logging_audit_failure", 3),
+ Category: "logging_audit_failure",
+ Name: "Log-Overflow ueberschreibt alte Eintraege",
+ Description: "Wenn der Log-Speicher voll ist, werden aeltere Eintraege ohne Warnung ueberschrieben, was eine lueckenlose Rueckverfolgung verhindert.",
+ DefaultSeverity: 3,
+ DefaultProbability: 4,
+ ApplicableComponentTypes: []string{"software", "controller"},
+ RegulationReferences: []string{"CRA", "IEC 62443"},
+ SuggestedMitigations: mustMarshalJSON([]string{"Log-Kapazitaetsalarm", "Externes Log-System", "Zirkulaerpuffer mit Warnschwelle"}),
+ IsBuiltin: true,
+ TenantID: nil,
+ CreatedAt: now,
+ },
+ {
+ ID: hazardUUID("logging_audit_failure", 4),
+ Category: "logging_audit_failure",
+ Name: "Fehlende Zeitstempel in Protokolleintraegen",
+ Description: "Log-Eintraege enthalten keine oder ungenaue Zeitstempel, was die zeitliche Rekonstruktion von Ereignissen bei der Fehlersuche verhindert.",
+ DefaultSeverity: 3,
+ DefaultProbability: 3,
+ ApplicableComponentTypes: []string{"software", "controller"},
+ RegulationReferences: []string{"CRA", "Maschinenverordnung 2023/1230"},
+ SuggestedMitigations: mustMarshalJSON([]string{"NTP-Synchronisation", "RTC im Geraet", "ISO-8601-Zeitstempel"}),
+ IsBuiltin: true,
+ TenantID: nil,
+ CreatedAt: now,
+ },
+ {
+ ID: hazardUUID("logging_audit_failure", 5),
+ Category: "logging_audit_failure",
+ Name: "Audit-Trail loeschbar durch Bediener",
+ Description: "Der Audit-Trail kann von einem normalen Bediener geloescht werden, was die Nachvollziehbarkeit von Sicherheitsereignissen untergaebt.",
+ DefaultSeverity: 4,
+ DefaultProbability: 2,
+ ApplicableComponentTypes: []string{"software"},
+ RegulationReferences: []string{"CRA", "IEC 62443", "Maschinenverordnung 2023/1230"},
+ SuggestedMitigations: mustMarshalJSON([]string{"RBAC: Nur Admin darf loeschen", "Log-Export vor Loeschung", "Unanderbare Log-Speicherung"}),
+ IsBuiltin: true,
+ TenantID: nil,
+ CreatedAt: now,
+ },
+
+ // ====================================================================
+ // Category: integration_error (8 entries)
+ // ====================================================================
+ {
+ ID: hazardUUID("integration_error", 1),
+ Category: "integration_error",
+ Name: "Datentyp-Mismatch an Schnittstelle",
+ Description: "Zwei Systeme tauschen Daten ueber eine Schnittstelle aus, die inkompatible Datentypen verwendet, was zu Interpretationsfehlern fuehrt.",
+ DefaultSeverity: 4,
+ DefaultProbability: 3,
+ ApplicableComponentTypes: []string{"software", "network"},
+ RegulationReferences: []string{"IEC 62304", "IEC 62443"},
+ SuggestedMitigations: mustMarshalJSON([]string{"Schnittstellendefinition (IDL/Protobuf)", "Integrationstests", "Datentypvalidierung"}),
+ IsBuiltin: true,
+ TenantID: nil,
+ CreatedAt: now,
+ },
+ {
+ ID: hazardUUID("integration_error", 2),
+ Category: "integration_error",
+ Name: "Endianness-Fehler bei Datenuebertragung",
+ Description: "Big-Endian- und Little-Endian-Systeme kommunizieren ohne Byte-Order-Konvertierung, was zu falsch interpretierten numerischen Werten fuehrt.",
+ DefaultSeverity: 4,
+ DefaultProbability: 3,
+ ApplicableComponentTypes: []string{"software", "network"},
+ RegulationReferences: []string{"IEC 62304", "IEC 62443"},
+ SuggestedMitigations: mustMarshalJSON([]string{"Explizite Byte-Order-Definiton", "Integrationstests", "Schnittstellenspezifikation"}),
+ IsBuiltin: true,
+ TenantID: nil,
+ CreatedAt: now,
+ },
+ {
+ ID: hazardUUID("integration_error", 3),
+ Category: "integration_error",
+ Name: "Protokoll-Versions-Konflikt",
+ Description: "Sender und Empfaenger verwenden unterschiedliche Protokollversionen, die nicht rueckwaertskompatibel sind, was zu Paketablehnung oder Fehlinterpretation fuehrt.",
+ DefaultSeverity: 4,
+ DefaultProbability: 3,
+ ApplicableComponentTypes: []string{"software", "network", "firmware"},
+ RegulationReferences: []string{"IEC 62443", "CRA"},
+ SuggestedMitigations: mustMarshalJSON([]string{"Versions-Aushandlung beim Verbindungsaufbau", "Backward-Compatibilitaet", "Kompatibilitaets-Matrix"}),
+ IsBuiltin: true,
+ TenantID: nil,
+ CreatedAt: now,
+ },
+ {
+ ID: hazardUUID("integration_error", 4),
+ Category: "integration_error",
+ Name: "Timeout nicht behandelt bei Kommunikation",
+ Description: "Eine Kommunikationsverbindung bricht ab oder antwortet nicht, der Sender erkennt dies nicht und wartet unendlich lang.",
+ DefaultSeverity: 4,
+ DefaultProbability: 3,
+ ApplicableComponentTypes: []string{"software", "network"},
+ RegulationReferences: []string{"IEC 62443", "Maschinenverordnung 2023/1230"},
+ SuggestedMitigations: mustMarshalJSON([]string{"Timeout-Konfiguration", "Watchdog-Timer", "Fail-Safe bei Verbindungsverlust"}),
+ IsBuiltin: true,
+ TenantID: nil,
+ CreatedAt: now,
+ },
+ {
+ ID: hazardUUID("integration_error", 5),
+ Category: "integration_error",
+ Name: "Buffer Overflow an Schnittstelle",
+ Description: "Eine Schnittstelle akzeptiert Eingaben, die groesser als der zugewiesene Puffer sind, was zu Speicher-Ueberschreibung und Kontrollfluss-Manipulation fuehrt.",
+ DefaultSeverity: 5,
+ DefaultProbability: 2,
+ ApplicableComponentTypes: []string{"software", "firmware", "network"},
+ RegulationReferences: []string{"CRA", "IEC 62443", "IEC 62304"},
+ SuggestedMitigations: mustMarshalJSON([]string{"Laengenvalidierung", "Sichere Puffer-Funktionen", "Statische Analyse (z.B. MISRA)"}),
+ IsBuiltin: true,
+ TenantID: nil,
+ CreatedAt: now,
+ },
+ {
+ ID: hazardUUID("integration_error", 6),
+ Category: "integration_error",
+ Name: "Fehlender Heartbeat bei Safety-Verbindung",
+ Description: "Eine Safety-Kommunikationsverbindung sendet keinen periodischen Heartbeat, so dass ein stiller Ausfall (z.B. unterbrochenes Kabel) nicht erkannt wird.",
+ DefaultSeverity: 5,
+ DefaultProbability: 2,
+ ApplicableComponentTypes: []string{"network", "software"},
+ RegulationReferences: []string{"IEC 61784-3", "ISO 13849", "IEC 62061"},
+ SuggestedMitigations: mustMarshalJSON([]string{"Heartbeat-Protokoll", "Verbindungsueberwachung", "Safe-State bei Heartbeat-Ausfall"}),
+ IsBuiltin: true,
+ TenantID: nil,
+ CreatedAt: now,
+ },
+ {
+ ID: hazardUUID("integration_error", 7),
+ Category: "integration_error",
+ Name: "Falscher Skalierungsfaktor bei Sensordaten",
+ Description: "Sensordaten werden mit einem falschen Faktor skaliert, was zu signifikant fehlerhaften Messwerten und moeglichen Fehlentscheidungen fuehrt.",
+ DefaultSeverity: 4,
+ DefaultProbability: 3,
+ ApplicableComponentTypes: []string{"sensor", "software"},
+ RegulationReferences: []string{"Maschinenverordnung 2023/1230", "IEC 62304"},
+ SuggestedMitigations: mustMarshalJSON([]string{"Kalibrierungspruefung", "Plausibilitaetstest", "Schnittstellendokumentation"}),
+ IsBuiltin: true,
+ TenantID: nil,
+ CreatedAt: now,
+ },
+ {
+ ID: hazardUUID("integration_error", 8),
+ Category: "integration_error",
+ Name: "Einheitenfehler (mm vs. inch)",
+ Description: "Unterschiedliche Masseinheiten zwischen Systemen fuehren zu fehlerhaften Bewegungsbefehlen oder Werkzeugpositionierungen.",
+ DefaultSeverity: 4,
+ DefaultProbability: 3,
+ ApplicableComponentTypes: []string{"software", "hmi"},
+ RegulationReferences: []string{"Maschinenverordnung 2023/1230", "IEC 62304"},
+ SuggestedMitigations: mustMarshalJSON([]string{"Explizite Einheitendefinition", "Einheitenkonvertierung in der Schnittstelle", "Integrationstests"}),
+ IsBuiltin: true,
+ TenantID: nil,
+ CreatedAt: now,
+ },
+ }
+}
diff --git a/ai-compliance-sdk/internal/iace/store.go b/ai-compliance-sdk/internal/iace/store.go
index 7d395a4..05899e5 100644
--- a/ai-compliance-sdk/internal/iace/store.go
+++ b/ai-compliance-sdk/internal/iace/store.go
@@ -1,13 +1,6 @@
package iace
import (
- "context"
- "encoding/json"
- "fmt"
- "time"
-
- "github.com/google/uuid"
- "github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgxpool"
)
@@ -20,1929 +13,3 @@ type Store struct {
func NewStore(pool *pgxpool.Pool) *Store {
return &Store{pool: pool}
}
-
-// ============================================================================
-// Project CRUD Operations
-// ============================================================================
-
-// CreateProject creates a new IACE project
-func (s *Store) CreateProject(ctx context.Context, tenantID uuid.UUID, req CreateProjectRequest) (*Project, error) {
- project := &Project{
- ID: uuid.New(),
- TenantID: tenantID,
- MachineName: req.MachineName,
- MachineType: req.MachineType,
- Manufacturer: req.Manufacturer,
- Description: req.Description,
- NarrativeText: req.NarrativeText,
- Status: ProjectStatusDraft,
- CEMarkingTarget: req.CEMarkingTarget,
- Metadata: req.Metadata,
- CreatedAt: time.Now().UTC(),
- UpdatedAt: time.Now().UTC(),
- }
-
- _, err := s.pool.Exec(ctx, `
- INSERT INTO iace_projects (
- id, tenant_id, machine_name, machine_type, manufacturer,
- description, narrative_text, status, ce_marking_target,
- completeness_score, risk_summary, triggered_regulations, metadata,
- created_at, updated_at, archived_at
- ) VALUES (
- $1, $2, $3, $4, $5,
- $6, $7, $8, $9,
- $10, $11, $12, $13,
- $14, $15, $16
- )
- `,
- project.ID, project.TenantID, project.MachineName, project.MachineType, project.Manufacturer,
- project.Description, project.NarrativeText, string(project.Status), project.CEMarkingTarget,
- project.CompletenessScore, nil, project.TriggeredRegulations, project.Metadata,
- project.CreatedAt, project.UpdatedAt, project.ArchivedAt,
- )
- if err != nil {
- return nil, fmt.Errorf("create project: %w", err)
- }
-
- return project, nil
-}
-
-// GetProject retrieves a project by ID
-func (s *Store) GetProject(ctx context.Context, id uuid.UUID) (*Project, error) {
- var p Project
- var status string
- var riskSummary, triggeredRegulations, metadata []byte
-
- err := s.pool.QueryRow(ctx, `
- SELECT
- id, tenant_id, machine_name, machine_type, manufacturer,
- description, narrative_text, status, ce_marking_target,
- completeness_score, risk_summary, triggered_regulations, metadata,
- created_at, updated_at, archived_at
- FROM iace_projects WHERE id = $1
- `, id).Scan(
- &p.ID, &p.TenantID, &p.MachineName, &p.MachineType, &p.Manufacturer,
- &p.Description, &p.NarrativeText, &status, &p.CEMarkingTarget,
- &p.CompletenessScore, &riskSummary, &triggeredRegulations, &metadata,
- &p.CreatedAt, &p.UpdatedAt, &p.ArchivedAt,
- )
- if err == pgx.ErrNoRows {
- return nil, nil
- }
- if err != nil {
- return nil, fmt.Errorf("get project: %w", err)
- }
-
- p.Status = ProjectStatus(status)
- json.Unmarshal(riskSummary, &p.RiskSummary)
- json.Unmarshal(triggeredRegulations, &p.TriggeredRegulations)
- json.Unmarshal(metadata, &p.Metadata)
-
- return &p, nil
-}
-
-// ListProjects lists all projects for a tenant
-func (s *Store) ListProjects(ctx context.Context, tenantID uuid.UUID) ([]Project, error) {
- rows, err := s.pool.Query(ctx, `
- SELECT
- id, tenant_id, machine_name, machine_type, manufacturer,
- description, narrative_text, status, ce_marking_target,
- completeness_score, risk_summary, triggered_regulations, metadata,
- created_at, updated_at, archived_at
- FROM iace_projects WHERE tenant_id = $1
- ORDER BY created_at DESC
- `, tenantID)
- if err != nil {
- return nil, fmt.Errorf("list projects: %w", err)
- }
- defer rows.Close()
-
- var projects []Project
- for rows.Next() {
- var p Project
- var status string
- var riskSummary, triggeredRegulations, metadata []byte
-
- err := rows.Scan(
- &p.ID, &p.TenantID, &p.MachineName, &p.MachineType, &p.Manufacturer,
- &p.Description, &p.NarrativeText, &status, &p.CEMarkingTarget,
- &p.CompletenessScore, &riskSummary, &triggeredRegulations, &metadata,
- &p.CreatedAt, &p.UpdatedAt, &p.ArchivedAt,
- )
- if err != nil {
- return nil, fmt.Errorf("list projects scan: %w", err)
- }
-
- p.Status = ProjectStatus(status)
- json.Unmarshal(riskSummary, &p.RiskSummary)
- json.Unmarshal(triggeredRegulations, &p.TriggeredRegulations)
- json.Unmarshal(metadata, &p.Metadata)
-
- projects = append(projects, p)
- }
-
- return projects, nil
-}
-
-// UpdateProject updates an existing project's mutable fields
-func (s *Store) UpdateProject(ctx context.Context, id uuid.UUID, req UpdateProjectRequest) (*Project, error) {
- // Fetch current project first
- project, err := s.GetProject(ctx, id)
- if err != nil {
- return nil, err
- }
- if project == nil {
- return nil, nil
- }
-
- // Apply partial updates
- if req.MachineName != nil {
- project.MachineName = *req.MachineName
- }
- if req.MachineType != nil {
- project.MachineType = *req.MachineType
- }
- if req.Manufacturer != nil {
- project.Manufacturer = *req.Manufacturer
- }
- if req.Description != nil {
- project.Description = *req.Description
- }
- if req.NarrativeText != nil {
- project.NarrativeText = *req.NarrativeText
- }
- if req.CEMarkingTarget != nil {
- project.CEMarkingTarget = *req.CEMarkingTarget
- }
- if req.Metadata != nil {
- project.Metadata = *req.Metadata
- }
-
- project.UpdatedAt = time.Now().UTC()
-
- _, err = s.pool.Exec(ctx, `
- UPDATE iace_projects SET
- machine_name = $2, machine_type = $3, manufacturer = $4,
- description = $5, narrative_text = $6, ce_marking_target = $7,
- metadata = $8, updated_at = $9
- WHERE id = $1
- `,
- id, project.MachineName, project.MachineType, project.Manufacturer,
- project.Description, project.NarrativeText, project.CEMarkingTarget,
- project.Metadata, project.UpdatedAt,
- )
- if err != nil {
- return nil, fmt.Errorf("update project: %w", err)
- }
-
- return project, nil
-}
-
-// ArchiveProject sets the archived_at timestamp and status for a project
-func (s *Store) ArchiveProject(ctx context.Context, id uuid.UUID) error {
- now := time.Now().UTC()
- _, err := s.pool.Exec(ctx, `
- UPDATE iace_projects SET
- status = $2, archived_at = $3, updated_at = $3
- WHERE id = $1
- `, id, string(ProjectStatusArchived), now)
- if err != nil {
- return fmt.Errorf("archive project: %w", err)
- }
- return nil
-}
-
-// UpdateProjectStatus updates the lifecycle status of a project
-func (s *Store) UpdateProjectStatus(ctx context.Context, id uuid.UUID, status ProjectStatus) error {
- _, err := s.pool.Exec(ctx, `
- UPDATE iace_projects SET status = $2, updated_at = NOW()
- WHERE id = $1
- `, id, string(status))
- if err != nil {
- return fmt.Errorf("update project status: %w", err)
- }
- return nil
-}
-
-// UpdateProjectCompleteness updates the completeness score and risk summary
-func (s *Store) UpdateProjectCompleteness(ctx context.Context, id uuid.UUID, score float64, riskSummary map[string]int) error {
- riskSummaryJSON, err := json.Marshal(riskSummary)
- if err != nil {
- return fmt.Errorf("marshal risk summary: %w", err)
- }
-
- _, err = s.pool.Exec(ctx, `
- UPDATE iace_projects SET
- completeness_score = $2, risk_summary = $3, updated_at = NOW()
- WHERE id = $1
- `, id, score, riskSummaryJSON)
- if err != nil {
- return fmt.Errorf("update project completeness: %w", err)
- }
- return nil
-}
-
-// ============================================================================
-// Component CRUD Operations
-// ============================================================================
-
-// CreateComponent creates a new component within a project
-func (s *Store) CreateComponent(ctx context.Context, req CreateComponentRequest) (*Component, error) {
- comp := &Component{
- ID: uuid.New(),
- ProjectID: req.ProjectID,
- ParentID: req.ParentID,
- Name: req.Name,
- ComponentType: req.ComponentType,
- Version: req.Version,
- Description: req.Description,
- IsSafetyRelevant: req.IsSafetyRelevant,
- IsNetworked: req.IsNetworked,
- CreatedAt: time.Now().UTC(),
- UpdatedAt: time.Now().UTC(),
- }
-
- _, err := s.pool.Exec(ctx, `
- INSERT INTO iace_components (
- id, project_id, parent_id, name, component_type,
- version, description, is_safety_relevant, is_networked,
- metadata, sort_order, created_at, updated_at
- ) VALUES (
- $1, $2, $3, $4, $5,
- $6, $7, $8, $9,
- $10, $11, $12, $13
- )
- `,
- comp.ID, comp.ProjectID, comp.ParentID, comp.Name, string(comp.ComponentType),
- comp.Version, comp.Description, comp.IsSafetyRelevant, comp.IsNetworked,
- comp.Metadata, comp.SortOrder, comp.CreatedAt, comp.UpdatedAt,
- )
- if err != nil {
- return nil, fmt.Errorf("create component: %w", err)
- }
-
- return comp, nil
-}
-
-// GetComponent retrieves a component by ID
-func (s *Store) GetComponent(ctx context.Context, id uuid.UUID) (*Component, error) {
- var c Component
- var compType string
- var metadata []byte
-
- err := s.pool.QueryRow(ctx, `
- SELECT
- id, project_id, parent_id, name, component_type,
- version, description, is_safety_relevant, is_networked,
- metadata, sort_order, created_at, updated_at
- FROM iace_components WHERE id = $1
- `, id).Scan(
- &c.ID, &c.ProjectID, &c.ParentID, &c.Name, &compType,
- &c.Version, &c.Description, &c.IsSafetyRelevant, &c.IsNetworked,
- &metadata, &c.SortOrder, &c.CreatedAt, &c.UpdatedAt,
- )
- if err == pgx.ErrNoRows {
- return nil, nil
- }
- if err != nil {
- return nil, fmt.Errorf("get component: %w", err)
- }
-
- c.ComponentType = ComponentType(compType)
- json.Unmarshal(metadata, &c.Metadata)
-
- return &c, nil
-}
-
-// ListComponents lists all components for a project
-func (s *Store) ListComponents(ctx context.Context, projectID uuid.UUID) ([]Component, error) {
- rows, err := s.pool.Query(ctx, `
- SELECT
- id, project_id, parent_id, name, component_type,
- version, description, is_safety_relevant, is_networked,
- metadata, sort_order, created_at, updated_at
- FROM iace_components WHERE project_id = $1
- ORDER BY sort_order ASC, created_at ASC
- `, projectID)
- if err != nil {
- return nil, fmt.Errorf("list components: %w", err)
- }
- defer rows.Close()
-
- var components []Component
- for rows.Next() {
- var c Component
- var compType string
- var metadata []byte
-
- err := rows.Scan(
- &c.ID, &c.ProjectID, &c.ParentID, &c.Name, &compType,
- &c.Version, &c.Description, &c.IsSafetyRelevant, &c.IsNetworked,
- &metadata, &c.SortOrder, &c.CreatedAt, &c.UpdatedAt,
- )
- if err != nil {
- return nil, fmt.Errorf("list components scan: %w", err)
- }
-
- c.ComponentType = ComponentType(compType)
- json.Unmarshal(metadata, &c.Metadata)
-
- components = append(components, c)
- }
-
- return components, nil
-}
-
-// UpdateComponent updates a component with a dynamic set of fields
-func (s *Store) UpdateComponent(ctx context.Context, id uuid.UUID, updates map[string]interface{}) (*Component, error) {
- if len(updates) == 0 {
- return s.GetComponent(ctx, id)
- }
-
- query := "UPDATE iace_components SET updated_at = NOW()"
- args := []interface{}{id}
- argIdx := 2
-
- for key, val := range updates {
- switch key {
- case "name", "version", "description":
- query += fmt.Sprintf(", %s = $%d", key, argIdx)
- args = append(args, val)
- argIdx++
- case "component_type":
- query += fmt.Sprintf(", component_type = $%d", argIdx)
- args = append(args, val)
- argIdx++
- case "is_safety_relevant":
- query += fmt.Sprintf(", is_safety_relevant = $%d", argIdx)
- args = append(args, val)
- argIdx++
- case "is_networked":
- query += fmt.Sprintf(", is_networked = $%d", argIdx)
- args = append(args, val)
- argIdx++
- case "sort_order":
- query += fmt.Sprintf(", sort_order = $%d", argIdx)
- args = append(args, val)
- argIdx++
- case "metadata":
- metaJSON, _ := json.Marshal(val)
- query += fmt.Sprintf(", metadata = $%d", argIdx)
- args = append(args, metaJSON)
- argIdx++
- case "parent_id":
- query += fmt.Sprintf(", parent_id = $%d", argIdx)
- args = append(args, val)
- argIdx++
- }
- }
-
- query += " WHERE id = $1"
-
- _, err := s.pool.Exec(ctx, query, args...)
- if err != nil {
- return nil, fmt.Errorf("update component: %w", err)
- }
-
- return s.GetComponent(ctx, id)
-}
-
-// DeleteComponent deletes a component by ID
-func (s *Store) DeleteComponent(ctx context.Context, id uuid.UUID) error {
- _, err := s.pool.Exec(ctx, "DELETE FROM iace_components WHERE id = $1", id)
- if err != nil {
- return fmt.Errorf("delete component: %w", err)
- }
- return nil
-}
-
-// ============================================================================
-// Classification Operations
-// ============================================================================
-
-// UpsertClassification inserts or updates a regulatory classification for a project
-func (s *Store) UpsertClassification(ctx context.Context, projectID uuid.UUID, regulation RegulationType, result string, riskLevel string, confidence float64, reasoning string, ragSources, requirements json.RawMessage) (*RegulatoryClassification, error) {
- id := uuid.New()
- now := time.Now().UTC()
-
- _, err := s.pool.Exec(ctx, `
- INSERT INTO iace_classifications (
- id, project_id, regulation, classification_result,
- risk_level, confidence, reasoning,
- rag_sources, requirements,
- created_at, updated_at
- ) VALUES (
- $1, $2, $3, $4,
- $5, $6, $7,
- $8, $9,
- $10, $11
- )
- ON CONFLICT (project_id, regulation)
- DO UPDATE SET
- classification_result = EXCLUDED.classification_result,
- risk_level = EXCLUDED.risk_level,
- confidence = EXCLUDED.confidence,
- reasoning = EXCLUDED.reasoning,
- rag_sources = EXCLUDED.rag_sources,
- requirements = EXCLUDED.requirements,
- updated_at = EXCLUDED.updated_at
- `,
- id, projectID, string(regulation), result,
- riskLevel, confidence, reasoning,
- ragSources, requirements,
- now, now,
- )
- if err != nil {
- return nil, fmt.Errorf("upsert classification: %w", err)
- }
-
- // Retrieve the upserted row (may have kept the original ID on conflict)
- return s.getClassificationByProjectAndRegulation(ctx, projectID, regulation)
-}
-
-// getClassificationByProjectAndRegulation is a helper to fetch a single classification
-func (s *Store) getClassificationByProjectAndRegulation(ctx context.Context, projectID uuid.UUID, regulation RegulationType) (*RegulatoryClassification, error) {
- var c RegulatoryClassification
- var reg, rl string
- var ragSources, requirements []byte
-
- err := s.pool.QueryRow(ctx, `
- SELECT
- id, project_id, regulation, classification_result,
- risk_level, confidence, reasoning,
- rag_sources, requirements,
- created_at, updated_at
- FROM iace_classifications
- WHERE project_id = $1 AND regulation = $2
- `, projectID, string(regulation)).Scan(
- &c.ID, &c.ProjectID, ®, &c.ClassificationResult,
- &rl, &c.Confidence, &c.Reasoning,
- &ragSources, &requirements,
- &c.CreatedAt, &c.UpdatedAt,
- )
- if err == pgx.ErrNoRows {
- return nil, nil
- }
- if err != nil {
- return nil, fmt.Errorf("get classification: %w", err)
- }
-
- c.Regulation = RegulationType(reg)
- c.RiskLevel = RiskLevel(rl)
- json.Unmarshal(ragSources, &c.RAGSources)
- json.Unmarshal(requirements, &c.Requirements)
-
- return &c, nil
-}
-
-// GetClassifications retrieves all classifications for a project
-func (s *Store) GetClassifications(ctx context.Context, projectID uuid.UUID) ([]RegulatoryClassification, error) {
- rows, err := s.pool.Query(ctx, `
- SELECT
- id, project_id, regulation, classification_result,
- risk_level, confidence, reasoning,
- rag_sources, requirements,
- created_at, updated_at
- FROM iace_classifications
- WHERE project_id = $1
- ORDER BY regulation ASC
- `, projectID)
- if err != nil {
- return nil, fmt.Errorf("get classifications: %w", err)
- }
- defer rows.Close()
-
- var classifications []RegulatoryClassification
- for rows.Next() {
- var c RegulatoryClassification
- var reg, rl string
- var ragSources, requirements []byte
-
- err := rows.Scan(
- &c.ID, &c.ProjectID, ®, &c.ClassificationResult,
- &rl, &c.Confidence, &c.Reasoning,
- &ragSources, &requirements,
- &c.CreatedAt, &c.UpdatedAt,
- )
- if err != nil {
- return nil, fmt.Errorf("get classifications scan: %w", err)
- }
-
- c.Regulation = RegulationType(reg)
- c.RiskLevel = RiskLevel(rl)
- json.Unmarshal(ragSources, &c.RAGSources)
- json.Unmarshal(requirements, &c.Requirements)
-
- classifications = append(classifications, c)
- }
-
- return classifications, nil
-}
-
-// ============================================================================
-// Hazard CRUD Operations
-// ============================================================================
-
-// CreateHazard creates a new hazard within a project
-func (s *Store) CreateHazard(ctx context.Context, req CreateHazardRequest) (*Hazard, error) {
- h := &Hazard{
- ID: uuid.New(),
- ProjectID: req.ProjectID,
- ComponentID: req.ComponentID,
- LibraryHazardID: req.LibraryHazardID,
- Name: req.Name,
- Description: req.Description,
- Scenario: req.Scenario,
- Category: req.Category,
- SubCategory: req.SubCategory,
- Status: HazardStatusIdentified,
- MachineModule: req.MachineModule,
- Function: req.Function,
- LifecyclePhase: req.LifecyclePhase,
- HazardousZone: req.HazardousZone,
- TriggerEvent: req.TriggerEvent,
- AffectedPerson: req.AffectedPerson,
- PossibleHarm: req.PossibleHarm,
- ReviewStatus: ReviewStatusDraft,
- CreatedAt: time.Now().UTC(),
- UpdatedAt: time.Now().UTC(),
- }
-
- _, err := s.pool.Exec(ctx, `
- INSERT INTO iace_hazards (
- id, project_id, component_id, library_hazard_id,
- name, description, scenario, category, sub_category, status,
- machine_module, function, lifecycle_phase, hazardous_zone,
- trigger_event, affected_person, possible_harm, review_status,
- 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
- )
- `,
- h.ID, h.ProjectID, h.ComponentID, h.LibraryHazardID,
- h.Name, h.Description, h.Scenario, h.Category, h.SubCategory, string(h.Status),
- h.MachineModule, h.Function, h.LifecyclePhase, h.HazardousZone,
- h.TriggerEvent, h.AffectedPerson, h.PossibleHarm, string(h.ReviewStatus),
- h.CreatedAt, h.UpdatedAt,
- )
- if err != nil {
- return nil, fmt.Errorf("create hazard: %w", err)
- }
-
- return h, nil
-}
-
-// GetHazard retrieves a hazard by ID
-func (s *Store) GetHazard(ctx context.Context, id uuid.UUID) (*Hazard, error) {
- var h Hazard
- var status, reviewStatus string
-
- err := s.pool.QueryRow(ctx, `
- SELECT
- id, project_id, component_id, library_hazard_id,
- name, description, scenario, category, sub_category, status,
- machine_module, function, lifecycle_phase, hazardous_zone,
- trigger_event, affected_person, possible_harm, review_status,
- created_at, updated_at
- FROM iace_hazards WHERE id = $1
- `, id).Scan(
- &h.ID, &h.ProjectID, &h.ComponentID, &h.LibraryHazardID,
- &h.Name, &h.Description, &h.Scenario, &h.Category, &h.SubCategory, &status,
- &h.MachineModule, &h.Function, &h.LifecyclePhase, &h.HazardousZone,
- &h.TriggerEvent, &h.AffectedPerson, &h.PossibleHarm, &reviewStatus,
- &h.CreatedAt, &h.UpdatedAt,
- )
- if err == pgx.ErrNoRows {
- return nil, nil
- }
- if err != nil {
- return nil, fmt.Errorf("get hazard: %w", err)
- }
-
- h.Status = HazardStatus(status)
- h.ReviewStatus = ReviewStatus(reviewStatus)
- return &h, nil
-}
-
-// ListHazards lists all hazards for a project
-func (s *Store) ListHazards(ctx context.Context, projectID uuid.UUID) ([]Hazard, error) {
- rows, err := s.pool.Query(ctx, `
- SELECT
- id, project_id, component_id, library_hazard_id,
- name, description, scenario, category, sub_category, status,
- machine_module, function, lifecycle_phase, hazardous_zone,
- trigger_event, affected_person, possible_harm, review_status,
- created_at, updated_at
- FROM iace_hazards WHERE project_id = $1
- ORDER BY created_at ASC
- `, projectID)
- if err != nil {
- return nil, fmt.Errorf("list hazards: %w", err)
- }
- defer rows.Close()
-
- var hazards []Hazard
- for rows.Next() {
- var h Hazard
- var status, reviewStatus string
-
- err := rows.Scan(
- &h.ID, &h.ProjectID, &h.ComponentID, &h.LibraryHazardID,
- &h.Name, &h.Description, &h.Scenario, &h.Category, &h.SubCategory, &status,
- &h.MachineModule, &h.Function, &h.LifecyclePhase, &h.HazardousZone,
- &h.TriggerEvent, &h.AffectedPerson, &h.PossibleHarm, &reviewStatus,
- &h.CreatedAt, &h.UpdatedAt,
- )
- if err != nil {
- return nil, fmt.Errorf("list hazards scan: %w", err)
- }
-
- h.Status = HazardStatus(status)
- h.ReviewStatus = ReviewStatus(reviewStatus)
- hazards = append(hazards, h)
- }
-
- return hazards, nil
-}
-
-// UpdateHazard updates a hazard with a dynamic set of fields
-func (s *Store) UpdateHazard(ctx context.Context, id uuid.UUID, updates map[string]interface{}) (*Hazard, error) {
- if len(updates) == 0 {
- return s.GetHazard(ctx, id)
- }
-
- query := "UPDATE iace_hazards SET updated_at = NOW()"
- args := []interface{}{id}
- argIdx := 2
-
- allowedFields := map[string]bool{
- "name": true, "description": true, "scenario": true, "category": true,
- "sub_category": true, "status": true, "component_id": true,
- "machine_module": true, "function": true, "lifecycle_phase": true,
- "hazardous_zone": true, "trigger_event": true, "affected_person": true,
- "possible_harm": true, "review_status": true,
- }
-
- for key, val := range updates {
- if allowedFields[key] {
- query += fmt.Sprintf(", %s = $%d", key, argIdx)
- args = append(args, val)
- argIdx++
- }
- }
-
- query += " WHERE id = $1"
-
- _, err := s.pool.Exec(ctx, query, args...)
- if err != nil {
- return nil, fmt.Errorf("update hazard: %w", err)
- }
-
- return s.GetHazard(ctx, id)
-}
-
-// ============================================================================
-// Risk Assessment Operations
-// ============================================================================
-
-// CreateRiskAssessment creates a new risk assessment for a hazard
-func (s *Store) CreateRiskAssessment(ctx context.Context, assessment *RiskAssessment) error {
- if assessment.ID == uuid.Nil {
- assessment.ID = uuid.New()
- }
- if assessment.CreatedAt.IsZero() {
- assessment.CreatedAt = time.Now().UTC()
- }
-
- _, err := s.pool.Exec(ctx, `
- INSERT INTO iace_risk_assessments (
- id, hazard_id, version, assessment_type,
- severity, exposure, probability,
- inherent_risk, control_maturity, control_coverage,
- test_evidence_strength, c_eff, residual_risk,
- risk_level, is_acceptable, acceptance_justification,
- assessed_by, created_at
- ) VALUES (
- $1, $2, $3, $4,
- $5, $6, $7,
- $8, $9, $10,
- $11, $12, $13,
- $14, $15, $16,
- $17, $18
- )
- `,
- assessment.ID, assessment.HazardID, assessment.Version, string(assessment.AssessmentType),
- assessment.Severity, assessment.Exposure, assessment.Probability,
- assessment.InherentRisk, assessment.ControlMaturity, assessment.ControlCoverage,
- assessment.TestEvidenceStrength, assessment.CEff, assessment.ResidualRisk,
- string(assessment.RiskLevel), assessment.IsAcceptable, assessment.AcceptanceJustification,
- assessment.AssessedBy, assessment.CreatedAt,
- )
- if err != nil {
- return fmt.Errorf("create risk assessment: %w", err)
- }
-
- return nil
-}
-
-// GetLatestAssessment retrieves the most recent risk assessment for a hazard
-func (s *Store) GetLatestAssessment(ctx context.Context, hazardID uuid.UUID) (*RiskAssessment, error) {
- var a RiskAssessment
- var assessmentType, riskLevel string
-
- err := s.pool.QueryRow(ctx, `
- SELECT
- id, hazard_id, version, assessment_type,
- severity, exposure, probability,
- inherent_risk, control_maturity, control_coverage,
- test_evidence_strength, c_eff, residual_risk,
- risk_level, is_acceptable, acceptance_justification,
- assessed_by, created_at
- FROM iace_risk_assessments
- WHERE hazard_id = $1
- ORDER BY version DESC, created_at DESC
- LIMIT 1
- `, hazardID).Scan(
- &a.ID, &a.HazardID, &a.Version, &assessmentType,
- &a.Severity, &a.Exposure, &a.Probability,
- &a.InherentRisk, &a.ControlMaturity, &a.ControlCoverage,
- &a.TestEvidenceStrength, &a.CEff, &a.ResidualRisk,
- &riskLevel, &a.IsAcceptable, &a.AcceptanceJustification,
- &a.AssessedBy, &a.CreatedAt,
- )
- if err == pgx.ErrNoRows {
- return nil, nil
- }
- if err != nil {
- return nil, fmt.Errorf("get latest assessment: %w", err)
- }
-
- a.AssessmentType = AssessmentType(assessmentType)
- a.RiskLevel = RiskLevel(riskLevel)
-
- return &a, nil
-}
-
-// ListAssessments lists all risk assessments for a hazard, newest first
-func (s *Store) ListAssessments(ctx context.Context, hazardID uuid.UUID) ([]RiskAssessment, error) {
- rows, err := s.pool.Query(ctx, `
- SELECT
- id, hazard_id, version, assessment_type,
- severity, exposure, probability,
- inherent_risk, control_maturity, control_coverage,
- test_evidence_strength, c_eff, residual_risk,
- risk_level, is_acceptable, acceptance_justification,
- assessed_by, created_at
- FROM iace_risk_assessments
- WHERE hazard_id = $1
- ORDER BY version DESC, created_at DESC
- `, hazardID)
- if err != nil {
- return nil, fmt.Errorf("list assessments: %w", err)
- }
- defer rows.Close()
-
- var assessments []RiskAssessment
- for rows.Next() {
- var a RiskAssessment
- var assessmentType, riskLevel string
-
- err := rows.Scan(
- &a.ID, &a.HazardID, &a.Version, &assessmentType,
- &a.Severity, &a.Exposure, &a.Probability,
- &a.InherentRisk, &a.ControlMaturity, &a.ControlCoverage,
- &a.TestEvidenceStrength, &a.CEff, &a.ResidualRisk,
- &riskLevel, &a.IsAcceptable, &a.AcceptanceJustification,
- &a.AssessedBy, &a.CreatedAt,
- )
- if err != nil {
- return nil, fmt.Errorf("list assessments scan: %w", err)
- }
-
- a.AssessmentType = AssessmentType(assessmentType)
- a.RiskLevel = RiskLevel(riskLevel)
-
- assessments = append(assessments, a)
- }
-
- return assessments, nil
-}
-
-// ============================================================================
-// Mitigation CRUD Operations
-// ============================================================================
-
-// CreateMitigation creates a new mitigation measure for a hazard
-func (s *Store) CreateMitigation(ctx context.Context, req CreateMitigationRequest) (*Mitigation, error) {
- m := &Mitigation{
- ID: uuid.New(),
- HazardID: req.HazardID,
- ReductionType: req.ReductionType,
- Name: req.Name,
- Description: req.Description,
- Status: MitigationStatusPlanned,
- CreatedAt: time.Now().UTC(),
- UpdatedAt: time.Now().UTC(),
- }
-
- _, err := s.pool.Exec(ctx, `
- INSERT INTO iace_mitigations (
- id, hazard_id, reduction_type, name, description,
- status, verification_method, verification_result,
- verified_at, verified_by,
- created_at, updated_at
- ) VALUES (
- $1, $2, $3, $4, $5,
- $6, $7, $8,
- $9, $10,
- $11, $12
- )
- `,
- m.ID, m.HazardID, string(m.ReductionType), m.Name, m.Description,
- string(m.Status), "", "",
- nil, uuid.Nil,
- m.CreatedAt, m.UpdatedAt,
- )
- if err != nil {
- return nil, fmt.Errorf("create mitigation: %w", err)
- }
-
- return m, nil
-}
-
-// UpdateMitigation updates a mitigation with a dynamic set of fields
-func (s *Store) UpdateMitigation(ctx context.Context, id uuid.UUID, updates map[string]interface{}) (*Mitigation, error) {
- if len(updates) == 0 {
- return s.getMitigation(ctx, id)
- }
-
- query := "UPDATE iace_mitigations SET updated_at = NOW()"
- args := []interface{}{id}
- argIdx := 2
-
- for key, val := range updates {
- switch key {
- case "name", "description", "verification_result":
- query += fmt.Sprintf(", %s = $%d", key, argIdx)
- args = append(args, val)
- argIdx++
- case "status":
- query += fmt.Sprintf(", status = $%d", argIdx)
- args = append(args, val)
- argIdx++
- case "reduction_type":
- query += fmt.Sprintf(", reduction_type = $%d", argIdx)
- args = append(args, val)
- argIdx++
- case "verification_method":
- query += fmt.Sprintf(", verification_method = $%d", argIdx)
- args = append(args, val)
- argIdx++
- }
- }
-
- query += " WHERE id = $1"
-
- _, err := s.pool.Exec(ctx, query, args...)
- if err != nil {
- return nil, fmt.Errorf("update mitigation: %w", err)
- }
-
- return s.getMitigation(ctx, id)
-}
-
-// VerifyMitigation marks a mitigation as verified
-func (s *Store) VerifyMitigation(ctx context.Context, id uuid.UUID, verificationResult string, verifiedBy string) error {
- now := time.Now().UTC()
- verifiedByUUID, err := uuid.Parse(verifiedBy)
- if err != nil {
- return fmt.Errorf("invalid verified_by UUID: %w", err)
- }
-
- _, err = s.pool.Exec(ctx, `
- UPDATE iace_mitigations SET
- status = $2,
- verification_result = $3,
- verified_at = $4,
- verified_by = $5,
- updated_at = $4
- WHERE id = $1
- `, id, string(MitigationStatusVerified), verificationResult, now, verifiedByUUID)
- if err != nil {
- return fmt.Errorf("verify mitigation: %w", err)
- }
-
- return nil
-}
-
-// ListMitigations lists all mitigations for a hazard
-func (s *Store) ListMitigations(ctx context.Context, hazardID uuid.UUID) ([]Mitigation, error) {
- rows, err := s.pool.Query(ctx, `
- SELECT
- id, hazard_id, reduction_type, name, description,
- status, verification_method, verification_result,
- verified_at, verified_by,
- created_at, updated_at
- FROM iace_mitigations WHERE hazard_id = $1
- ORDER BY created_at ASC
- `, hazardID)
- if err != nil {
- return nil, fmt.Errorf("list mitigations: %w", err)
- }
- defer rows.Close()
-
- var mitigations []Mitigation
- for rows.Next() {
- var m Mitigation
- var reductionType, status, verificationMethod string
-
- err := rows.Scan(
- &m.ID, &m.HazardID, &reductionType, &m.Name, &m.Description,
- &status, &verificationMethod, &m.VerificationResult,
- &m.VerifiedAt, &m.VerifiedBy,
- &m.CreatedAt, &m.UpdatedAt,
- )
- if err != nil {
- return nil, fmt.Errorf("list mitigations scan: %w", err)
- }
-
- m.ReductionType = ReductionType(reductionType)
- m.Status = MitigationStatus(status)
- m.VerificationMethod = VerificationMethod(verificationMethod)
-
- mitigations = append(mitigations, m)
- }
-
- return mitigations, nil
-}
-
-// GetMitigation fetches a single mitigation by ID.
-func (s *Store) GetMitigation(ctx context.Context, id uuid.UUID) (*Mitigation, error) {
- return s.getMitigation(ctx, id)
-}
-
-// getMitigation is a helper to fetch a single mitigation by ID
-func (s *Store) getMitigation(ctx context.Context, id uuid.UUID) (*Mitigation, error) {
- var m Mitigation
- var reductionType, status, verificationMethod string
-
- err := s.pool.QueryRow(ctx, `
- SELECT
- id, hazard_id, reduction_type, name, description,
- status, verification_method, verification_result,
- verified_at, verified_by,
- created_at, updated_at
- FROM iace_mitigations WHERE id = $1
- `, id).Scan(
- &m.ID, &m.HazardID, &reductionType, &m.Name, &m.Description,
- &status, &verificationMethod, &m.VerificationResult,
- &m.VerifiedAt, &m.VerifiedBy,
- &m.CreatedAt, &m.UpdatedAt,
- )
- if err == pgx.ErrNoRows {
- return nil, nil
- }
- if err != nil {
- return nil, fmt.Errorf("get mitigation: %w", err)
- }
-
- m.ReductionType = ReductionType(reductionType)
- m.Status = MitigationStatus(status)
- m.VerificationMethod = VerificationMethod(verificationMethod)
-
- return &m, nil
-}
-
-// ============================================================================
-// Evidence Operations
-// ============================================================================
-
-// CreateEvidence creates a new evidence record
-func (s *Store) CreateEvidence(ctx context.Context, evidence *Evidence) error {
- if evidence.ID == uuid.Nil {
- evidence.ID = uuid.New()
- }
- if evidence.CreatedAt.IsZero() {
- evidence.CreatedAt = time.Now().UTC()
- }
-
- _, err := s.pool.Exec(ctx, `
- INSERT INTO iace_evidence (
- id, project_id, mitigation_id, verification_plan_id,
- file_name, file_path, file_hash, file_size, mime_type,
- description, uploaded_by, created_at
- ) VALUES (
- $1, $2, $3, $4,
- $5, $6, $7, $8, $9,
- $10, $11, $12
- )
- `,
- evidence.ID, evidence.ProjectID, evidence.MitigationID, evidence.VerificationPlanID,
- evidence.FileName, evidence.FilePath, evidence.FileHash, evidence.FileSize, evidence.MimeType,
- evidence.Description, evidence.UploadedBy, evidence.CreatedAt,
- )
- if err != nil {
- return fmt.Errorf("create evidence: %w", err)
- }
-
- return nil
-}
-
-// ListEvidence lists all evidence for a project
-func (s *Store) ListEvidence(ctx context.Context, projectID uuid.UUID) ([]Evidence, error) {
- rows, err := s.pool.Query(ctx, `
- SELECT
- id, project_id, mitigation_id, verification_plan_id,
- file_name, file_path, file_hash, file_size, mime_type,
- description, uploaded_by, created_at
- FROM iace_evidence WHERE project_id = $1
- ORDER BY created_at DESC
- `, projectID)
- if err != nil {
- return nil, fmt.Errorf("list evidence: %w", err)
- }
- defer rows.Close()
-
- var evidence []Evidence
- for rows.Next() {
- var e Evidence
-
- err := rows.Scan(
- &e.ID, &e.ProjectID, &e.MitigationID, &e.VerificationPlanID,
- &e.FileName, &e.FilePath, &e.FileHash, &e.FileSize, &e.MimeType,
- &e.Description, &e.UploadedBy, &e.CreatedAt,
- )
- if err != nil {
- return nil, fmt.Errorf("list evidence scan: %w", err)
- }
-
- evidence = append(evidence, e)
- }
-
- return evidence, nil
-}
-
-// ============================================================================
-// Verification Plan Operations
-// ============================================================================
-
-// CreateVerificationPlan creates a new verification plan
-func (s *Store) CreateVerificationPlan(ctx context.Context, req CreateVerificationPlanRequest) (*VerificationPlan, error) {
- vp := &VerificationPlan{
- ID: uuid.New(),
- ProjectID: req.ProjectID,
- HazardID: req.HazardID,
- MitigationID: req.MitigationID,
- Title: req.Title,
- Description: req.Description,
- AcceptanceCriteria: req.AcceptanceCriteria,
- Method: req.Method,
- Status: "planned",
- CreatedAt: time.Now().UTC(),
- UpdatedAt: time.Now().UTC(),
- }
-
- _, err := s.pool.Exec(ctx, `
- INSERT INTO iace_verification_plans (
- id, project_id, hazard_id, mitigation_id,
- title, description, acceptance_criteria, method,
- status, result, completed_at, completed_by,
- created_at, updated_at
- ) VALUES (
- $1, $2, $3, $4,
- $5, $6, $7, $8,
- $9, $10, $11, $12,
- $13, $14
- )
- `,
- vp.ID, vp.ProjectID, vp.HazardID, vp.MitigationID,
- vp.Title, vp.Description, vp.AcceptanceCriteria, string(vp.Method),
- vp.Status, "", nil, uuid.Nil,
- vp.CreatedAt, vp.UpdatedAt,
- )
- if err != nil {
- return nil, fmt.Errorf("create verification plan: %w", err)
- }
-
- return vp, nil
-}
-
-// UpdateVerificationPlan updates a verification plan with a dynamic set of fields
-func (s *Store) UpdateVerificationPlan(ctx context.Context, id uuid.UUID, updates map[string]interface{}) (*VerificationPlan, error) {
- if len(updates) == 0 {
- return s.getVerificationPlan(ctx, id)
- }
-
- query := "UPDATE iace_verification_plans SET updated_at = NOW()"
- args := []interface{}{id}
- argIdx := 2
-
- for key, val := range updates {
- switch key {
- case "title", "description", "acceptance_criteria", "result", "status":
- query += fmt.Sprintf(", %s = $%d", key, argIdx)
- args = append(args, val)
- argIdx++
- case "method":
- query += fmt.Sprintf(", method = $%d", argIdx)
- args = append(args, val)
- argIdx++
- }
- }
-
- query += " WHERE id = $1"
-
- _, err := s.pool.Exec(ctx, query, args...)
- if err != nil {
- return nil, fmt.Errorf("update verification plan: %w", err)
- }
-
- return s.getVerificationPlan(ctx, id)
-}
-
-// CompleteVerification marks a verification plan as completed
-func (s *Store) CompleteVerification(ctx context.Context, id uuid.UUID, result string, completedBy string) error {
- now := time.Now().UTC()
- completedByUUID, err := uuid.Parse(completedBy)
- if err != nil {
- return fmt.Errorf("invalid completed_by UUID: %w", err)
- }
-
- _, err = s.pool.Exec(ctx, `
- UPDATE iace_verification_plans SET
- status = 'completed',
- result = $2,
- completed_at = $3,
- completed_by = $4,
- updated_at = $3
- WHERE id = $1
- `, id, result, now, completedByUUID)
- if err != nil {
- return fmt.Errorf("complete verification: %w", err)
- }
-
- return nil
-}
-
-// ListVerificationPlans lists all verification plans for a project
-func (s *Store) ListVerificationPlans(ctx context.Context, projectID uuid.UUID) ([]VerificationPlan, error) {
- rows, err := s.pool.Query(ctx, `
- SELECT
- id, project_id, hazard_id, mitigation_id,
- title, description, acceptance_criteria, method,
- status, result, completed_at, completed_by,
- created_at, updated_at
- FROM iace_verification_plans WHERE project_id = $1
- ORDER BY created_at ASC
- `, projectID)
- if err != nil {
- return nil, fmt.Errorf("list verification plans: %w", err)
- }
- defer rows.Close()
-
- var plans []VerificationPlan
- for rows.Next() {
- var vp VerificationPlan
- var method string
-
- err := rows.Scan(
- &vp.ID, &vp.ProjectID, &vp.HazardID, &vp.MitigationID,
- &vp.Title, &vp.Description, &vp.AcceptanceCriteria, &method,
- &vp.Status, &vp.Result, &vp.CompletedAt, &vp.CompletedBy,
- &vp.CreatedAt, &vp.UpdatedAt,
- )
- if err != nil {
- return nil, fmt.Errorf("list verification plans scan: %w", err)
- }
-
- vp.Method = VerificationMethod(method)
- plans = append(plans, vp)
- }
-
- return plans, nil
-}
-
-// getVerificationPlan is a helper to fetch a single verification plan by ID
-func (s *Store) getVerificationPlan(ctx context.Context, id uuid.UUID) (*VerificationPlan, error) {
- var vp VerificationPlan
- var method string
-
- err := s.pool.QueryRow(ctx, `
- SELECT
- id, project_id, hazard_id, mitigation_id,
- title, description, acceptance_criteria, method,
- status, result, completed_at, completed_by,
- created_at, updated_at
- FROM iace_verification_plans WHERE id = $1
- `, id).Scan(
- &vp.ID, &vp.ProjectID, &vp.HazardID, &vp.MitigationID,
- &vp.Title, &vp.Description, &vp.AcceptanceCriteria, &method,
- &vp.Status, &vp.Result, &vp.CompletedAt, &vp.CompletedBy,
- &vp.CreatedAt, &vp.UpdatedAt,
- )
- if err == pgx.ErrNoRows {
- return nil, nil
- }
- if err != nil {
- return nil, fmt.Errorf("get verification plan: %w", err)
- }
-
- vp.Method = VerificationMethod(method)
- return &vp, nil
-}
-
-// ============================================================================
-// Tech File Section Operations
-// ============================================================================
-
-// CreateTechFileSection creates a new section in the technical file
-func (s *Store) CreateTechFileSection(ctx context.Context, projectID uuid.UUID, sectionType, title, content string) (*TechFileSection, error) {
- tf := &TechFileSection{
- ID: uuid.New(),
- ProjectID: projectID,
- SectionType: sectionType,
- Title: title,
- Content: content,
- Version: 1,
- Status: TechFileSectionStatusDraft,
- CreatedAt: time.Now().UTC(),
- UpdatedAt: time.Now().UTC(),
- }
-
- _, err := s.pool.Exec(ctx, `
- INSERT INTO iace_tech_file_sections (
- id, project_id, section_type, title, content,
- version, status, approved_by, approved_at, metadata,
- created_at, updated_at
- ) VALUES (
- $1, $2, $3, $4, $5,
- $6, $7, $8, $9, $10,
- $11, $12
- )
- `,
- tf.ID, tf.ProjectID, tf.SectionType, tf.Title, tf.Content,
- tf.Version, string(tf.Status), uuid.Nil, nil, nil,
- tf.CreatedAt, tf.UpdatedAt,
- )
- if err != nil {
- return nil, fmt.Errorf("create tech file section: %w", err)
- }
-
- return tf, nil
-}
-
-// UpdateTechFileSection updates the content of a tech file section and bumps version
-func (s *Store) UpdateTechFileSection(ctx context.Context, id uuid.UUID, content string) error {
- _, err := s.pool.Exec(ctx, `
- UPDATE iace_tech_file_sections SET
- content = $2,
- version = version + 1,
- status = $3,
- updated_at = NOW()
- WHERE id = $1
- `, id, content, string(TechFileSectionStatusDraft))
- if err != nil {
- return fmt.Errorf("update tech file section: %w", err)
- }
- return nil
-}
-
-// ApproveTechFileSection marks a tech file section as approved
-func (s *Store) ApproveTechFileSection(ctx context.Context, id uuid.UUID, approvedBy string) error {
- now := time.Now().UTC()
- approvedByUUID, err := uuid.Parse(approvedBy)
- if err != nil {
- return fmt.Errorf("invalid approved_by UUID: %w", err)
- }
-
- _, err = s.pool.Exec(ctx, `
- UPDATE iace_tech_file_sections SET
- status = $2,
- approved_by = $3,
- approved_at = $4,
- updated_at = $4
- WHERE id = $1
- `, id, string(TechFileSectionStatusApproved), approvedByUUID, now)
- if err != nil {
- return fmt.Errorf("approve tech file section: %w", err)
- }
-
- return nil
-}
-
-// ListTechFileSections lists all tech file sections for a project
-func (s *Store) ListTechFileSections(ctx context.Context, projectID uuid.UUID) ([]TechFileSection, error) {
- rows, err := s.pool.Query(ctx, `
- SELECT
- id, project_id, section_type, title, content,
- version, status, approved_by, approved_at, metadata,
- created_at, updated_at
- FROM iace_tech_file_sections WHERE project_id = $1
- ORDER BY section_type ASC, created_at ASC
- `, projectID)
- if err != nil {
- return nil, fmt.Errorf("list tech file sections: %w", err)
- }
- defer rows.Close()
-
- var sections []TechFileSection
- for rows.Next() {
- var tf TechFileSection
- var status string
- var metadata []byte
-
- err := rows.Scan(
- &tf.ID, &tf.ProjectID, &tf.SectionType, &tf.Title, &tf.Content,
- &tf.Version, &status, &tf.ApprovedBy, &tf.ApprovedAt, &metadata,
- &tf.CreatedAt, &tf.UpdatedAt,
- )
- if err != nil {
- return nil, fmt.Errorf("list tech file sections scan: %w", err)
- }
-
- tf.Status = TechFileSectionStatus(status)
- json.Unmarshal(metadata, &tf.Metadata)
-
- sections = append(sections, tf)
- }
-
- return sections, nil
-}
-
-// ============================================================================
-// Monitoring Event Operations
-// ============================================================================
-
-// CreateMonitoringEvent creates a new post-market monitoring event
-func (s *Store) CreateMonitoringEvent(ctx context.Context, req CreateMonitoringEventRequest) (*MonitoringEvent, error) {
- me := &MonitoringEvent{
- ID: uuid.New(),
- ProjectID: req.ProjectID,
- EventType: req.EventType,
- Title: req.Title,
- Description: req.Description,
- Severity: req.Severity,
- Status: "open",
- CreatedAt: time.Now().UTC(),
- UpdatedAt: time.Now().UTC(),
- }
-
- _, err := s.pool.Exec(ctx, `
- INSERT INTO iace_monitoring_events (
- id, project_id, event_type, title, description,
- severity, impact_assessment, status,
- resolved_at, resolved_by, metadata,
- created_at, updated_at
- ) VALUES (
- $1, $2, $3, $4, $5,
- $6, $7, $8,
- $9, $10, $11,
- $12, $13
- )
- `,
- me.ID, me.ProjectID, string(me.EventType), me.Title, me.Description,
- me.Severity, "", me.Status,
- nil, uuid.Nil, nil,
- me.CreatedAt, me.UpdatedAt,
- )
- if err != nil {
- return nil, fmt.Errorf("create monitoring event: %w", err)
- }
-
- return me, nil
-}
-
-// ListMonitoringEvents lists all monitoring events for a project
-func (s *Store) ListMonitoringEvents(ctx context.Context, projectID uuid.UUID) ([]MonitoringEvent, error) {
- rows, err := s.pool.Query(ctx, `
- SELECT
- id, project_id, event_type, title, description,
- severity, impact_assessment, status,
- resolved_at, resolved_by, metadata,
- created_at, updated_at
- FROM iace_monitoring_events WHERE project_id = $1
- ORDER BY created_at DESC
- `, projectID)
- if err != nil {
- return nil, fmt.Errorf("list monitoring events: %w", err)
- }
- defer rows.Close()
-
- var events []MonitoringEvent
- for rows.Next() {
- var me MonitoringEvent
- var eventType string
- var metadata []byte
-
- err := rows.Scan(
- &me.ID, &me.ProjectID, &eventType, &me.Title, &me.Description,
- &me.Severity, &me.ImpactAssessment, &me.Status,
- &me.ResolvedAt, &me.ResolvedBy, &metadata,
- &me.CreatedAt, &me.UpdatedAt,
- )
- if err != nil {
- return nil, fmt.Errorf("list monitoring events scan: %w", err)
- }
-
- me.EventType = MonitoringEventType(eventType)
- json.Unmarshal(metadata, &me.Metadata)
-
- events = append(events, me)
- }
-
- return events, nil
-}
-
-// UpdateMonitoringEvent updates a monitoring event with a dynamic set of fields
-func (s *Store) UpdateMonitoringEvent(ctx context.Context, id uuid.UUID, updates map[string]interface{}) (*MonitoringEvent, error) {
- if len(updates) == 0 {
- return s.getMonitoringEvent(ctx, id)
- }
-
- query := "UPDATE iace_monitoring_events SET updated_at = NOW()"
- args := []interface{}{id}
- argIdx := 2
-
- for key, val := range updates {
- switch key {
- case "title", "description", "severity", "impact_assessment", "status":
- query += fmt.Sprintf(", %s = $%d", key, argIdx)
- args = append(args, val)
- argIdx++
- case "event_type":
- query += fmt.Sprintf(", event_type = $%d", argIdx)
- args = append(args, val)
- argIdx++
- case "resolved_at":
- query += fmt.Sprintf(", resolved_at = $%d", argIdx)
- args = append(args, val)
- argIdx++
- case "resolved_by":
- query += fmt.Sprintf(", resolved_by = $%d", argIdx)
- args = append(args, val)
- argIdx++
- case "metadata":
- metaJSON, _ := json.Marshal(val)
- query += fmt.Sprintf(", metadata = $%d", argIdx)
- args = append(args, metaJSON)
- argIdx++
- }
- }
-
- query += " WHERE id = $1"
-
- _, err := s.pool.Exec(ctx, query, args...)
- if err != nil {
- return nil, fmt.Errorf("update monitoring event: %w", err)
- }
-
- return s.getMonitoringEvent(ctx, id)
-}
-
-// getMonitoringEvent is a helper to fetch a single monitoring event by ID
-func (s *Store) getMonitoringEvent(ctx context.Context, id uuid.UUID) (*MonitoringEvent, error) {
- var me MonitoringEvent
- var eventType string
- var metadata []byte
-
- err := s.pool.QueryRow(ctx, `
- SELECT
- id, project_id, event_type, title, description,
- severity, impact_assessment, status,
- resolved_at, resolved_by, metadata,
- created_at, updated_at
- FROM iace_monitoring_events WHERE id = $1
- `, id).Scan(
- &me.ID, &me.ProjectID, &eventType, &me.Title, &me.Description,
- &me.Severity, &me.ImpactAssessment, &me.Status,
- &me.ResolvedAt, &me.ResolvedBy, &metadata,
- &me.CreatedAt, &me.UpdatedAt,
- )
- if err == pgx.ErrNoRows {
- return nil, nil
- }
- if err != nil {
- return nil, fmt.Errorf("get monitoring event: %w", err)
- }
-
- me.EventType = MonitoringEventType(eventType)
- json.Unmarshal(metadata, &me.Metadata)
-
- return &me, nil
-}
-
-// ============================================================================
-// Audit Trail Operations
-// ============================================================================
-
-// AddAuditEntry adds an immutable audit trail entry
-func (s *Store) AddAuditEntry(ctx context.Context, projectID uuid.UUID, entityType string, entityID uuid.UUID, action AuditAction, userID string, oldValues, newValues json.RawMessage) error {
- id := uuid.New()
- now := time.Now().UTC()
-
- userUUID, err := uuid.Parse(userID)
- if err != nil {
- return fmt.Errorf("invalid user_id UUID: %w", err)
- }
-
- // Compute a simple hash for integrity: sha256(entityType + entityID + action + timestamp)
- hashInput := fmt.Sprintf("%s:%s:%s:%s:%s", projectID, entityType, entityID, string(action), now.Format(time.RFC3339Nano))
- // Use a simple deterministic hash representation
- hash := fmt.Sprintf("%x", hashInput)
-
- _, err = s.pool.Exec(ctx, `
- INSERT INTO iace_audit_trail (
- id, project_id, entity_type, entity_id,
- action, user_id, old_values, new_values,
- hash, created_at
- ) VALUES (
- $1, $2, $3, $4,
- $5, $6, $7, $8,
- $9, $10
- )
- `,
- id, projectID, entityType, entityID,
- string(action), userUUID, oldValues, newValues,
- hash, now,
- )
- if err != nil {
- return fmt.Errorf("add audit entry: %w", err)
- }
-
- return nil
-}
-
-// ListAuditTrail lists all audit trail entries for a project, newest first
-func (s *Store) ListAuditTrail(ctx context.Context, projectID uuid.UUID) ([]AuditTrailEntry, error) {
- rows, err := s.pool.Query(ctx, `
- SELECT
- id, project_id, entity_type, entity_id,
- action, user_id, old_values, new_values,
- hash, created_at
- FROM iace_audit_trail WHERE project_id = $1
- ORDER BY created_at DESC
- `, projectID)
- if err != nil {
- return nil, fmt.Errorf("list audit trail: %w", err)
- }
- defer rows.Close()
-
- var entries []AuditTrailEntry
- for rows.Next() {
- var e AuditTrailEntry
- var action string
-
- err := rows.Scan(
- &e.ID, &e.ProjectID, &e.EntityType, &e.EntityID,
- &action, &e.UserID, &e.OldValues, &e.NewValues,
- &e.Hash, &e.CreatedAt,
- )
- if err != nil {
- return nil, fmt.Errorf("list audit trail scan: %w", err)
- }
-
- e.Action = AuditAction(action)
- entries = append(entries, e)
- }
-
- return entries, nil
-}
-
-// HasAuditEntryForType checks if an audit trail entry exists for the given entity type within a project.
-func (s *Store) HasAuditEntryForType(ctx context.Context, projectID uuid.UUID, entityType string) (bool, error) {
- var exists bool
- err := s.pool.QueryRow(ctx, `
- SELECT EXISTS(
- SELECT 1 FROM iace_audit_trail
- WHERE project_id = $1 AND entity_type = $2
- )
- `, projectID, entityType).Scan(&exists)
- if err != nil {
- return false, fmt.Errorf("has audit entry: %w", err)
- }
- return exists, nil
-}
-
-// ============================================================================
-// Hazard Library Operations
-// ============================================================================
-
-// ListHazardLibrary lists hazard library entries, optionally filtered by category and component type
-func (s *Store) ListHazardLibrary(ctx context.Context, category string, componentType string) ([]HazardLibraryEntry, error) {
- query := `
- SELECT
- id, category, COALESCE(sub_category, ''), name, description,
- default_severity, default_probability,
- COALESCE(default_exposure, 3), COALESCE(default_avoidance, 3),
- applicable_component_types, regulation_references,
- suggested_mitigations,
- COALESCE(typical_causes, '[]'::jsonb),
- COALESCE(typical_harm, ''),
- COALESCE(relevant_lifecycle_phases, '[]'::jsonb),
- COALESCE(recommended_measures_design, '[]'::jsonb),
- COALESCE(recommended_measures_technical, '[]'::jsonb),
- COALESCE(recommended_measures_information, '[]'::jsonb),
- COALESCE(suggested_evidence, '[]'::jsonb),
- COALESCE(related_keywords, '[]'::jsonb),
- is_builtin, tenant_id,
- created_at
- FROM iace_hazard_library WHERE 1=1`
-
- args := []interface{}{}
- argIdx := 1
-
- if category != "" {
- query += fmt.Sprintf(" AND category = $%d", argIdx)
- args = append(args, category)
- argIdx++
- }
- if componentType != "" {
- query += fmt.Sprintf(" AND applicable_component_types @> $%d::jsonb", argIdx)
- componentTypeJSON, _ := json.Marshal([]string{componentType})
- args = append(args, string(componentTypeJSON))
- argIdx++
- }
-
- query += " ORDER BY category ASC, name ASC"
-
- rows, err := s.pool.Query(ctx, query, args...)
- if err != nil {
- return nil, fmt.Errorf("list hazard library: %w", err)
- }
- defer rows.Close()
-
- var entries []HazardLibraryEntry
- for rows.Next() {
- var e HazardLibraryEntry
- var applicableComponentTypes, regulationReferences, suggestedMitigations []byte
- var typicalCauses, relevantPhases, measuresDesign, measuresTechnical, measuresInfo, evidence, keywords []byte
-
- err := rows.Scan(
- &e.ID, &e.Category, &e.SubCategory, &e.Name, &e.Description,
- &e.DefaultSeverity, &e.DefaultProbability,
- &e.DefaultExposure, &e.DefaultAvoidance,
- &applicableComponentTypes, ®ulationReferences,
- &suggestedMitigations,
- &typicalCauses, &e.TypicalHarm, &relevantPhases,
- &measuresDesign, &measuresTechnical, &measuresInfo,
- &evidence, &keywords,
- &e.IsBuiltin, &e.TenantID,
- &e.CreatedAt,
- )
- if err != nil {
- return nil, fmt.Errorf("list hazard library scan: %w", err)
- }
-
- json.Unmarshal(applicableComponentTypes, &e.ApplicableComponentTypes)
- json.Unmarshal(regulationReferences, &e.RegulationReferences)
- json.Unmarshal(suggestedMitigations, &e.SuggestedMitigations)
- json.Unmarshal(typicalCauses, &e.TypicalCauses)
- json.Unmarshal(relevantPhases, &e.RelevantLifecyclePhases)
- json.Unmarshal(measuresDesign, &e.RecommendedMeasuresDesign)
- json.Unmarshal(measuresTechnical, &e.RecommendedMeasuresTechnical)
- json.Unmarshal(measuresInfo, &e.RecommendedMeasuresInformation)
- json.Unmarshal(evidence, &e.SuggestedEvidence)
- json.Unmarshal(keywords, &e.RelatedKeywords)
-
- if e.ApplicableComponentTypes == nil {
- e.ApplicableComponentTypes = []string{}
- }
- if e.RegulationReferences == nil {
- e.RegulationReferences = []string{}
- }
-
- entries = append(entries, e)
- }
-
- return entries, nil
-}
-
-// GetHazardLibraryEntry retrieves a single hazard library entry by ID
-func (s *Store) GetHazardLibraryEntry(ctx context.Context, id uuid.UUID) (*HazardLibraryEntry, error) {
- var e HazardLibraryEntry
- var applicableComponentTypes, regulationReferences, suggestedMitigations []byte
- var typicalCauses, relevantLifecyclePhases []byte
- var recommendedMeasuresDesign, recommendedMeasuresTechnical, recommendedMeasuresInformation []byte
- var suggestedEvidence, relatedKeywords []byte
-
- err := s.pool.QueryRow(ctx, `
- SELECT
- id, category, name, description,
- default_severity, default_probability,
- applicable_component_types, regulation_references,
- suggested_mitigations, is_builtin, tenant_id,
- created_at,
- COALESCE(sub_category, ''),
- COALESCE(default_exposure, 3),
- COALESCE(default_avoidance, 3),
- COALESCE(typical_causes, '[]'),
- COALESCE(typical_harm, ''),
- COALESCE(relevant_lifecycle_phases, '[]'),
- COALESCE(recommended_measures_design, '[]'),
- COALESCE(recommended_measures_technical, '[]'),
- COALESCE(recommended_measures_information, '[]'),
- COALESCE(suggested_evidence, '[]'),
- COALESCE(related_keywords, '[]')
- FROM iace_hazard_library WHERE id = $1
- `, id).Scan(
- &e.ID, &e.Category, &e.Name, &e.Description,
- &e.DefaultSeverity, &e.DefaultProbability,
- &applicableComponentTypes, ®ulationReferences,
- &suggestedMitigations, &e.IsBuiltin, &e.TenantID,
- &e.CreatedAt,
- &e.SubCategory,
- &e.DefaultExposure, &e.DefaultAvoidance,
- &typicalCauses, &e.TypicalHarm,
- &relevantLifecyclePhases,
- &recommendedMeasuresDesign, &recommendedMeasuresTechnical, &recommendedMeasuresInformation,
- &suggestedEvidence, &relatedKeywords,
- )
- if err == pgx.ErrNoRows {
- return nil, nil
- }
- if err != nil {
- return nil, fmt.Errorf("get hazard library entry: %w", err)
- }
-
- json.Unmarshal(applicableComponentTypes, &e.ApplicableComponentTypes)
- json.Unmarshal(regulationReferences, &e.RegulationReferences)
- json.Unmarshal(suggestedMitigations, &e.SuggestedMitigations)
- json.Unmarshal(typicalCauses, &e.TypicalCauses)
- json.Unmarshal(relevantLifecyclePhases, &e.RelevantLifecyclePhases)
- json.Unmarshal(recommendedMeasuresDesign, &e.RecommendedMeasuresDesign)
- json.Unmarshal(recommendedMeasuresTechnical, &e.RecommendedMeasuresTechnical)
- json.Unmarshal(recommendedMeasuresInformation, &e.RecommendedMeasuresInformation)
- json.Unmarshal(suggestedEvidence, &e.SuggestedEvidence)
- json.Unmarshal(relatedKeywords, &e.RelatedKeywords)
-
- if e.ApplicableComponentTypes == nil {
- e.ApplicableComponentTypes = []string{}
- }
- if e.RegulationReferences == nil {
- e.RegulationReferences = []string{}
- }
-
- return &e, nil
-}
-
-// ============================================================================
-// Risk Summary (Aggregated View)
-// ============================================================================
-
-// GetRiskSummary computes an aggregated risk overview for a project
-func (s *Store) GetRiskSummary(ctx context.Context, projectID uuid.UUID) (*RiskSummaryResponse, error) {
- // Get all hazards for the project
- hazards, err := s.ListHazards(ctx, projectID)
- if err != nil {
- return nil, fmt.Errorf("get risk summary - list hazards: %w", err)
- }
-
- summary := &RiskSummaryResponse{
- TotalHazards: len(hazards),
- AllAcceptable: true,
- }
-
- if len(hazards) == 0 {
- summary.OverallRiskLevel = RiskLevelNegligible
- return summary, nil
- }
-
- highestRisk := RiskLevelNegligible
-
- for _, h := range hazards {
- latest, err := s.GetLatestAssessment(ctx, h.ID)
- if err != nil {
- return nil, fmt.Errorf("get risk summary - get assessment for hazard %s: %w", h.ID, err)
- }
- if latest == nil {
- // Hazard without assessment counts as unassessed; consider it not acceptable
- summary.AllAcceptable = false
- continue
- }
-
- switch latest.RiskLevel {
- case RiskLevelNotAcceptable:
- summary.NotAcceptable++
- case RiskLevelVeryHigh:
- summary.VeryHigh++
- case RiskLevelCritical:
- summary.Critical++
- case RiskLevelHigh:
- summary.High++
- case RiskLevelMedium:
- summary.Medium++
- case RiskLevelLow:
- summary.Low++
- case RiskLevelNegligible:
- summary.Negligible++
- }
-
- if !latest.IsAcceptable {
- summary.AllAcceptable = false
- }
-
- // Track highest risk level
- if riskLevelSeverity(latest.RiskLevel) > riskLevelSeverity(highestRisk) {
- highestRisk = latest.RiskLevel
- }
- }
-
- summary.OverallRiskLevel = highestRisk
-
- return summary, nil
-}
-
-// riskLevelSeverity returns a numeric severity for risk level comparison
-func riskLevelSeverity(rl RiskLevel) int {
- switch rl {
- case RiskLevelNotAcceptable:
- return 7
- case RiskLevelVeryHigh:
- return 6
- case RiskLevelCritical:
- return 5
- case RiskLevelHigh:
- return 4
- case RiskLevelMedium:
- return 3
- case RiskLevelLow:
- return 2
- case RiskLevelNegligible:
- return 1
- default:
- return 0
- }
-}
-
-// ListLifecyclePhases returns all 12 lifecycle phases with DE/EN labels
-func (s *Store) ListLifecyclePhases(ctx context.Context) ([]LifecyclePhaseInfo, error) {
- rows, err := s.pool.Query(ctx, `
- SELECT id, label_de, label_en, sort_order
- FROM iace_lifecycle_phases
- ORDER BY sort_order ASC
- `)
- if err != nil {
- return nil, fmt.Errorf("list lifecycle phases: %w", err)
- }
- defer rows.Close()
-
- var phases []LifecyclePhaseInfo
- for rows.Next() {
- var p LifecyclePhaseInfo
- if err := rows.Scan(&p.ID, &p.LabelDE, &p.LabelEN, &p.Sort); err != nil {
- return nil, fmt.Errorf("list lifecycle phases scan: %w", err)
- }
- phases = append(phases, p)
- }
- return phases, nil
-}
-
-// ListRoles returns all affected person roles from the reference table
-func (s *Store) ListRoles(ctx context.Context) ([]RoleInfo, error) {
- rows, err := s.pool.Query(ctx, `
- SELECT id, label_de, label_en, sort_order
- FROM iace_roles
- ORDER BY sort_order ASC
- `)
- if err != nil {
- return nil, fmt.Errorf("list roles: %w", err)
- }
- defer rows.Close()
-
- var roles []RoleInfo
- for rows.Next() {
- var r RoleInfo
- if err := rows.Scan(&r.ID, &r.LabelDE, &r.LabelEN, &r.Sort); err != nil {
- return nil, fmt.Errorf("list roles scan: %w", err)
- }
- roles = append(roles, r)
- }
- return roles, nil
-}
-
-// ListEvidenceTypes returns all evidence types from the reference table
-func (s *Store) ListEvidenceTypes(ctx context.Context) ([]EvidenceTypeInfo, error) {
- rows, err := s.pool.Query(ctx, `
- SELECT id, category, label_de, label_en, sort_order
- FROM iace_evidence_types
- ORDER BY sort_order ASC
- `)
- if err != nil {
- return nil, fmt.Errorf("list evidence types: %w", err)
- }
- defer rows.Close()
-
- var types []EvidenceTypeInfo
- for rows.Next() {
- var e EvidenceTypeInfo
- if err := rows.Scan(&e.ID, &e.Category, &e.LabelDE, &e.LabelEN, &e.Sort); err != nil {
- return nil, fmt.Errorf("list evidence types scan: %w", err)
- }
- types = append(types, e)
- }
- return types, nil
-}
diff --git a/ai-compliance-sdk/internal/iace/store_audit.go b/ai-compliance-sdk/internal/iace/store_audit.go
new file mode 100644
index 0000000..7b927f5
--- /dev/null
+++ b/ai-compliance-sdk/internal/iace/store_audit.go
@@ -0,0 +1,383 @@
+package iace
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "time"
+
+ "github.com/google/uuid"
+ "github.com/jackc/pgx/v5"
+)
+
+// ============================================================================
+// Tech File Section Operations
+// ============================================================================
+
+// CreateTechFileSection creates a new section in the technical file
+func (s *Store) CreateTechFileSection(ctx context.Context, projectID uuid.UUID, sectionType, title, content string) (*TechFileSection, error) {
+ tf := &TechFileSection{
+ ID: uuid.New(),
+ ProjectID: projectID,
+ SectionType: sectionType,
+ Title: title,
+ Content: content,
+ Version: 1,
+ Status: TechFileSectionStatusDraft,
+ CreatedAt: time.Now().UTC(),
+ UpdatedAt: time.Now().UTC(),
+ }
+
+ _, err := s.pool.Exec(ctx, `
+ INSERT INTO iace_tech_file_sections (
+ id, project_id, section_type, title, content,
+ version, status, approved_by, approved_at, metadata,
+ created_at, updated_at
+ ) VALUES (
+ $1, $2, $3, $4, $5,
+ $6, $7, $8, $9, $10,
+ $11, $12
+ )
+ `,
+ tf.ID, tf.ProjectID, tf.SectionType, tf.Title, tf.Content,
+ tf.Version, string(tf.Status), uuid.Nil, nil, nil,
+ tf.CreatedAt, tf.UpdatedAt,
+ )
+ if err != nil {
+ return nil, fmt.Errorf("create tech file section: %w", err)
+ }
+
+ return tf, nil
+}
+
+// UpdateTechFileSection updates the content of a tech file section and bumps version
+func (s *Store) UpdateTechFileSection(ctx context.Context, id uuid.UUID, content string) error {
+ _, err := s.pool.Exec(ctx, `
+ UPDATE iace_tech_file_sections SET
+ content = $2,
+ version = version + 1,
+ status = $3,
+ updated_at = NOW()
+ WHERE id = $1
+ `, id, content, string(TechFileSectionStatusDraft))
+ if err != nil {
+ return fmt.Errorf("update tech file section: %w", err)
+ }
+ return nil
+}
+
+// ApproveTechFileSection marks a tech file section as approved
+func (s *Store) ApproveTechFileSection(ctx context.Context, id uuid.UUID, approvedBy string) error {
+ now := time.Now().UTC()
+ approvedByUUID, err := uuid.Parse(approvedBy)
+ if err != nil {
+ return fmt.Errorf("invalid approved_by UUID: %w", err)
+ }
+
+ _, err = s.pool.Exec(ctx, `
+ UPDATE iace_tech_file_sections SET
+ status = $2,
+ approved_by = $3,
+ approved_at = $4,
+ updated_at = $4
+ WHERE id = $1
+ `, id, string(TechFileSectionStatusApproved), approvedByUUID, now)
+ if err != nil {
+ return fmt.Errorf("approve tech file section: %w", err)
+ }
+
+ return nil
+}
+
+// ListTechFileSections lists all tech file sections for a project
+func (s *Store) ListTechFileSections(ctx context.Context, projectID uuid.UUID) ([]TechFileSection, error) {
+ rows, err := s.pool.Query(ctx, `
+ SELECT
+ id, project_id, section_type, title, content,
+ version, status, approved_by, approved_at, metadata,
+ created_at, updated_at
+ FROM iace_tech_file_sections WHERE project_id = $1
+ ORDER BY section_type ASC, created_at ASC
+ `, projectID)
+ if err != nil {
+ return nil, fmt.Errorf("list tech file sections: %w", err)
+ }
+ defer rows.Close()
+
+ var sections []TechFileSection
+ for rows.Next() {
+ var tf TechFileSection
+ var status string
+ var metadata []byte
+
+ err := rows.Scan(
+ &tf.ID, &tf.ProjectID, &tf.SectionType, &tf.Title, &tf.Content,
+ &tf.Version, &status, &tf.ApprovedBy, &tf.ApprovedAt, &metadata,
+ &tf.CreatedAt, &tf.UpdatedAt,
+ )
+ if err != nil {
+ return nil, fmt.Errorf("list tech file sections scan: %w", err)
+ }
+
+ tf.Status = TechFileSectionStatus(status)
+ json.Unmarshal(metadata, &tf.Metadata)
+
+ sections = append(sections, tf)
+ }
+
+ return sections, nil
+}
+
+// ============================================================================
+// Monitoring Event Operations
+// ============================================================================
+
+// CreateMonitoringEvent creates a new post-market monitoring event
+func (s *Store) CreateMonitoringEvent(ctx context.Context, req CreateMonitoringEventRequest) (*MonitoringEvent, error) {
+ me := &MonitoringEvent{
+ ID: uuid.New(),
+ ProjectID: req.ProjectID,
+ EventType: req.EventType,
+ Title: req.Title,
+ Description: req.Description,
+ Severity: req.Severity,
+ Status: "open",
+ CreatedAt: time.Now().UTC(),
+ UpdatedAt: time.Now().UTC(),
+ }
+
+ _, err := s.pool.Exec(ctx, `
+ INSERT INTO iace_monitoring_events (
+ id, project_id, event_type, title, description,
+ severity, impact_assessment, status,
+ resolved_at, resolved_by, metadata,
+ created_at, updated_at
+ ) VALUES (
+ $1, $2, $3, $4, $5,
+ $6, $7, $8,
+ $9, $10, $11,
+ $12, $13
+ )
+ `,
+ me.ID, me.ProjectID, string(me.EventType), me.Title, me.Description,
+ me.Severity, "", me.Status,
+ nil, uuid.Nil, nil,
+ me.CreatedAt, me.UpdatedAt,
+ )
+ if err != nil {
+ return nil, fmt.Errorf("create monitoring event: %w", err)
+ }
+
+ return me, nil
+}
+
+// ListMonitoringEvents lists all monitoring events for a project
+func (s *Store) ListMonitoringEvents(ctx context.Context, projectID uuid.UUID) ([]MonitoringEvent, error) {
+ rows, err := s.pool.Query(ctx, `
+ SELECT
+ id, project_id, event_type, title, description,
+ severity, impact_assessment, status,
+ resolved_at, resolved_by, metadata,
+ created_at, updated_at
+ FROM iace_monitoring_events WHERE project_id = $1
+ ORDER BY created_at DESC
+ `, projectID)
+ if err != nil {
+ return nil, fmt.Errorf("list monitoring events: %w", err)
+ }
+ defer rows.Close()
+
+ var events []MonitoringEvent
+ for rows.Next() {
+ var me MonitoringEvent
+ var eventType string
+ var metadata []byte
+
+ err := rows.Scan(
+ &me.ID, &me.ProjectID, &eventType, &me.Title, &me.Description,
+ &me.Severity, &me.ImpactAssessment, &me.Status,
+ &me.ResolvedAt, &me.ResolvedBy, &metadata,
+ &me.CreatedAt, &me.UpdatedAt,
+ )
+ if err != nil {
+ return nil, fmt.Errorf("list monitoring events scan: %w", err)
+ }
+
+ me.EventType = MonitoringEventType(eventType)
+ json.Unmarshal(metadata, &me.Metadata)
+
+ events = append(events, me)
+ }
+
+ return events, nil
+}
+
+// UpdateMonitoringEvent updates a monitoring event with a dynamic set of fields
+func (s *Store) UpdateMonitoringEvent(ctx context.Context, id uuid.UUID, updates map[string]interface{}) (*MonitoringEvent, error) {
+ if len(updates) == 0 {
+ return s.getMonitoringEvent(ctx, id)
+ }
+
+ query := "UPDATE iace_monitoring_events SET updated_at = NOW()"
+ args := []interface{}{id}
+ argIdx := 2
+
+ for key, val := range updates {
+ switch key {
+ case "title", "description", "severity", "impact_assessment", "status":
+ query += fmt.Sprintf(", %s = $%d", key, argIdx)
+ args = append(args, val)
+ argIdx++
+ case "event_type":
+ query += fmt.Sprintf(", event_type = $%d", argIdx)
+ args = append(args, val)
+ argIdx++
+ case "resolved_at":
+ query += fmt.Sprintf(", resolved_at = $%d", argIdx)
+ args = append(args, val)
+ argIdx++
+ case "resolved_by":
+ query += fmt.Sprintf(", resolved_by = $%d", argIdx)
+ args = append(args, val)
+ argIdx++
+ case "metadata":
+ metaJSON, _ := json.Marshal(val)
+ query += fmt.Sprintf(", metadata = $%d", argIdx)
+ args = append(args, metaJSON)
+ argIdx++
+ }
+ }
+
+ query += " WHERE id = $1"
+
+ _, err := s.pool.Exec(ctx, query, args...)
+ if err != nil {
+ return nil, fmt.Errorf("update monitoring event: %w", err)
+ }
+
+ return s.getMonitoringEvent(ctx, id)
+}
+
+// getMonitoringEvent is a helper to fetch a single monitoring event by ID
+func (s *Store) getMonitoringEvent(ctx context.Context, id uuid.UUID) (*MonitoringEvent, error) {
+ var me MonitoringEvent
+ var eventType string
+ var metadata []byte
+
+ err := s.pool.QueryRow(ctx, `
+ SELECT
+ id, project_id, event_type, title, description,
+ severity, impact_assessment, status,
+ resolved_at, resolved_by, metadata,
+ created_at, updated_at
+ FROM iace_monitoring_events WHERE id = $1
+ `, id).Scan(
+ &me.ID, &me.ProjectID, &eventType, &me.Title, &me.Description,
+ &me.Severity, &me.ImpactAssessment, &me.Status,
+ &me.ResolvedAt, &me.ResolvedBy, &metadata,
+ &me.CreatedAt, &me.UpdatedAt,
+ )
+ if err == pgx.ErrNoRows {
+ return nil, nil
+ }
+ if err != nil {
+ return nil, fmt.Errorf("get monitoring event: %w", err)
+ }
+
+ me.EventType = MonitoringEventType(eventType)
+ json.Unmarshal(metadata, &me.Metadata)
+
+ return &me, nil
+}
+
+// ============================================================================
+// Audit Trail Operations
+// ============================================================================
+
+// AddAuditEntry adds an immutable audit trail entry
+func (s *Store) AddAuditEntry(ctx context.Context, projectID uuid.UUID, entityType string, entityID uuid.UUID, action AuditAction, userID string, oldValues, newValues json.RawMessage) error {
+ id := uuid.New()
+ now := time.Now().UTC()
+
+ userUUID, err := uuid.Parse(userID)
+ if err != nil {
+ return fmt.Errorf("invalid user_id UUID: %w", err)
+ }
+
+ // Compute a simple hash for integrity: sha256(entityType + entityID + action + timestamp)
+ hashInput := fmt.Sprintf("%s:%s:%s:%s:%s", projectID, entityType, entityID, string(action), now.Format(time.RFC3339Nano))
+ // Use a simple deterministic hash representation
+ hash := fmt.Sprintf("%x", hashInput)
+
+ _, err = s.pool.Exec(ctx, `
+ INSERT INTO iace_audit_trail (
+ id, project_id, entity_type, entity_id,
+ action, user_id, old_values, new_values,
+ hash, created_at
+ ) VALUES (
+ $1, $2, $3, $4,
+ $5, $6, $7, $8,
+ $9, $10
+ )
+ `,
+ id, projectID, entityType, entityID,
+ string(action), userUUID, oldValues, newValues,
+ hash, now,
+ )
+ if err != nil {
+ return fmt.Errorf("add audit entry: %w", err)
+ }
+
+ return nil
+}
+
+// ListAuditTrail lists all audit trail entries for a project, newest first
+func (s *Store) ListAuditTrail(ctx context.Context, projectID uuid.UUID) ([]AuditTrailEntry, error) {
+ rows, err := s.pool.Query(ctx, `
+ SELECT
+ id, project_id, entity_type, entity_id,
+ action, user_id, old_values, new_values,
+ hash, created_at
+ FROM iace_audit_trail WHERE project_id = $1
+ ORDER BY created_at DESC
+ `, projectID)
+ if err != nil {
+ return nil, fmt.Errorf("list audit trail: %w", err)
+ }
+ defer rows.Close()
+
+ var entries []AuditTrailEntry
+ for rows.Next() {
+ var e AuditTrailEntry
+ var action string
+
+ err := rows.Scan(
+ &e.ID, &e.ProjectID, &e.EntityType, &e.EntityID,
+ &action, &e.UserID, &e.OldValues, &e.NewValues,
+ &e.Hash, &e.CreatedAt,
+ )
+ if err != nil {
+ return nil, fmt.Errorf("list audit trail scan: %w", err)
+ }
+
+ e.Action = AuditAction(action)
+ entries = append(entries, e)
+ }
+
+ return entries, nil
+}
+
+// HasAuditEntryForType checks if an audit trail entry exists for the given entity type within a project.
+func (s *Store) HasAuditEntryForType(ctx context.Context, projectID uuid.UUID, entityType string) (bool, error) {
+ var exists bool
+ err := s.pool.QueryRow(ctx, `
+ SELECT EXISTS(
+ SELECT 1 FROM iace_audit_trail
+ WHERE project_id = $1 AND entity_type = $2
+ )
+ `, projectID, entityType).Scan(&exists)
+ if err != nil {
+ return false, fmt.Errorf("has audit entry: %w", err)
+ }
+ return exists, nil
+}
diff --git a/ai-compliance-sdk/internal/iace/store_hazards.go b/ai-compliance-sdk/internal/iace/store_hazards.go
new file mode 100644
index 0000000..15afcd2
--- /dev/null
+++ b/ai-compliance-sdk/internal/iace/store_hazards.go
@@ -0,0 +1,555 @@
+package iace
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "time"
+
+ "github.com/google/uuid"
+ "github.com/jackc/pgx/v5"
+)
+
+// ============================================================================
+// Hazard CRUD Operations
+// ============================================================================
+
+// CreateHazard creates a new hazard within a project
+func (s *Store) CreateHazard(ctx context.Context, req CreateHazardRequest) (*Hazard, error) {
+ h := &Hazard{
+ ID: uuid.New(),
+ ProjectID: req.ProjectID,
+ ComponentID: req.ComponentID,
+ LibraryHazardID: req.LibraryHazardID,
+ Name: req.Name,
+ Description: req.Description,
+ Scenario: req.Scenario,
+ Category: req.Category,
+ SubCategory: req.SubCategory,
+ Status: HazardStatusIdentified,
+ MachineModule: req.MachineModule,
+ Function: req.Function,
+ LifecyclePhase: req.LifecyclePhase,
+ HazardousZone: req.HazardousZone,
+ TriggerEvent: req.TriggerEvent,
+ AffectedPerson: req.AffectedPerson,
+ PossibleHarm: req.PossibleHarm,
+ ReviewStatus: ReviewStatusDraft,
+ CreatedAt: time.Now().UTC(),
+ UpdatedAt: time.Now().UTC(),
+ }
+
+ _, err := s.pool.Exec(ctx, `
+ INSERT INTO iace_hazards (
+ id, project_id, component_id, library_hazard_id,
+ name, description, scenario, category, sub_category, status,
+ machine_module, function, lifecycle_phase, hazardous_zone,
+ trigger_event, affected_person, possible_harm, review_status,
+ 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
+ )
+ `,
+ h.ID, h.ProjectID, h.ComponentID, h.LibraryHazardID,
+ h.Name, h.Description, h.Scenario, h.Category, h.SubCategory, string(h.Status),
+ h.MachineModule, h.Function, h.LifecyclePhase, h.HazardousZone,
+ h.TriggerEvent, h.AffectedPerson, h.PossibleHarm, string(h.ReviewStatus),
+ h.CreatedAt, h.UpdatedAt,
+ )
+ if err != nil {
+ return nil, fmt.Errorf("create hazard: %w", err)
+ }
+
+ return h, nil
+}
+
+// GetHazard retrieves a hazard by ID
+func (s *Store) GetHazard(ctx context.Context, id uuid.UUID) (*Hazard, error) {
+ var h Hazard
+ var status, reviewStatus string
+
+ err := s.pool.QueryRow(ctx, `
+ SELECT
+ id, project_id, component_id, library_hazard_id,
+ name, description, scenario, category, sub_category, status,
+ machine_module, function, lifecycle_phase, hazardous_zone,
+ trigger_event, affected_person, possible_harm, review_status,
+ created_at, updated_at
+ FROM iace_hazards WHERE id = $1
+ `, id).Scan(
+ &h.ID, &h.ProjectID, &h.ComponentID, &h.LibraryHazardID,
+ &h.Name, &h.Description, &h.Scenario, &h.Category, &h.SubCategory, &status,
+ &h.MachineModule, &h.Function, &h.LifecyclePhase, &h.HazardousZone,
+ &h.TriggerEvent, &h.AffectedPerson, &h.PossibleHarm, &reviewStatus,
+ &h.CreatedAt, &h.UpdatedAt,
+ )
+ if err == pgx.ErrNoRows {
+ return nil, nil
+ }
+ if err != nil {
+ return nil, fmt.Errorf("get hazard: %w", err)
+ }
+
+ h.Status = HazardStatus(status)
+ h.ReviewStatus = ReviewStatus(reviewStatus)
+ return &h, nil
+}
+
+// ListHazards lists all hazards for a project
+func (s *Store) ListHazards(ctx context.Context, projectID uuid.UUID) ([]Hazard, error) {
+ rows, err := s.pool.Query(ctx, `
+ SELECT
+ id, project_id, component_id, library_hazard_id,
+ name, description, scenario, category, sub_category, status,
+ machine_module, function, lifecycle_phase, hazardous_zone,
+ trigger_event, affected_person, possible_harm, review_status,
+ created_at, updated_at
+ FROM iace_hazards WHERE project_id = $1
+ ORDER BY created_at ASC
+ `, projectID)
+ if err != nil {
+ return nil, fmt.Errorf("list hazards: %w", err)
+ }
+ defer rows.Close()
+
+ var hazards []Hazard
+ for rows.Next() {
+ var h Hazard
+ var status, reviewStatus string
+
+ err := rows.Scan(
+ &h.ID, &h.ProjectID, &h.ComponentID, &h.LibraryHazardID,
+ &h.Name, &h.Description, &h.Scenario, &h.Category, &h.SubCategory, &status,
+ &h.MachineModule, &h.Function, &h.LifecyclePhase, &h.HazardousZone,
+ &h.TriggerEvent, &h.AffectedPerson, &h.PossibleHarm, &reviewStatus,
+ &h.CreatedAt, &h.UpdatedAt,
+ )
+ if err != nil {
+ return nil, fmt.Errorf("list hazards scan: %w", err)
+ }
+
+ h.Status = HazardStatus(status)
+ h.ReviewStatus = ReviewStatus(reviewStatus)
+ hazards = append(hazards, h)
+ }
+
+ return hazards, nil
+}
+
+// UpdateHazard updates a hazard with a dynamic set of fields
+func (s *Store) UpdateHazard(ctx context.Context, id uuid.UUID, updates map[string]interface{}) (*Hazard, error) {
+ if len(updates) == 0 {
+ return s.GetHazard(ctx, id)
+ }
+
+ query := "UPDATE iace_hazards SET updated_at = NOW()"
+ args := []interface{}{id}
+ argIdx := 2
+
+ allowedFields := map[string]bool{
+ "name": true, "description": true, "scenario": true, "category": true,
+ "sub_category": true, "status": true, "component_id": true,
+ "machine_module": true, "function": true, "lifecycle_phase": true,
+ "hazardous_zone": true, "trigger_event": true, "affected_person": true,
+ "possible_harm": true, "review_status": true,
+ }
+
+ for key, val := range updates {
+ if allowedFields[key] {
+ query += fmt.Sprintf(", %s = $%d", key, argIdx)
+ args = append(args, val)
+ argIdx++
+ }
+ }
+
+ query += " WHERE id = $1"
+
+ _, err := s.pool.Exec(ctx, query, args...)
+ if err != nil {
+ return nil, fmt.Errorf("update hazard: %w", err)
+ }
+
+ return s.GetHazard(ctx, id)
+}
+
+// ============================================================================
+// Risk Assessment Operations
+// ============================================================================
+
+// CreateRiskAssessment creates a new risk assessment for a hazard
+func (s *Store) CreateRiskAssessment(ctx context.Context, assessment *RiskAssessment) error {
+ if assessment.ID == uuid.Nil {
+ assessment.ID = uuid.New()
+ }
+ if assessment.CreatedAt.IsZero() {
+ assessment.CreatedAt = time.Now().UTC()
+ }
+
+ _, err := s.pool.Exec(ctx, `
+ INSERT INTO iace_risk_assessments (
+ id, hazard_id, version, assessment_type,
+ severity, exposure, probability,
+ inherent_risk, control_maturity, control_coverage,
+ test_evidence_strength, c_eff, residual_risk,
+ risk_level, is_acceptable, acceptance_justification,
+ assessed_by, created_at
+ ) VALUES (
+ $1, $2, $3, $4,
+ $5, $6, $7,
+ $8, $9, $10,
+ $11, $12, $13,
+ $14, $15, $16,
+ $17, $18
+ )
+ `,
+ assessment.ID, assessment.HazardID, assessment.Version, string(assessment.AssessmentType),
+ assessment.Severity, assessment.Exposure, assessment.Probability,
+ assessment.InherentRisk, assessment.ControlMaturity, assessment.ControlCoverage,
+ assessment.TestEvidenceStrength, assessment.CEff, assessment.ResidualRisk,
+ string(assessment.RiskLevel), assessment.IsAcceptable, assessment.AcceptanceJustification,
+ assessment.AssessedBy, assessment.CreatedAt,
+ )
+ if err != nil {
+ return fmt.Errorf("create risk assessment: %w", err)
+ }
+
+ return nil
+}
+
+// GetLatestAssessment retrieves the most recent risk assessment for a hazard
+func (s *Store) GetLatestAssessment(ctx context.Context, hazardID uuid.UUID) (*RiskAssessment, error) {
+ var a RiskAssessment
+ var assessmentType, riskLevel string
+
+ err := s.pool.QueryRow(ctx, `
+ SELECT
+ id, hazard_id, version, assessment_type,
+ severity, exposure, probability,
+ inherent_risk, control_maturity, control_coverage,
+ test_evidence_strength, c_eff, residual_risk,
+ risk_level, is_acceptable, acceptance_justification,
+ assessed_by, created_at
+ FROM iace_risk_assessments
+ WHERE hazard_id = $1
+ ORDER BY version DESC, created_at DESC
+ LIMIT 1
+ `, hazardID).Scan(
+ &a.ID, &a.HazardID, &a.Version, &assessmentType,
+ &a.Severity, &a.Exposure, &a.Probability,
+ &a.InherentRisk, &a.ControlMaturity, &a.ControlCoverage,
+ &a.TestEvidenceStrength, &a.CEff, &a.ResidualRisk,
+ &riskLevel, &a.IsAcceptable, &a.AcceptanceJustification,
+ &a.AssessedBy, &a.CreatedAt,
+ )
+ if err == pgx.ErrNoRows {
+ return nil, nil
+ }
+ if err != nil {
+ return nil, fmt.Errorf("get latest assessment: %w", err)
+ }
+
+ a.AssessmentType = AssessmentType(assessmentType)
+ a.RiskLevel = RiskLevel(riskLevel)
+
+ return &a, nil
+}
+
+// ListAssessments lists all risk assessments for a hazard, newest first
+func (s *Store) ListAssessments(ctx context.Context, hazardID uuid.UUID) ([]RiskAssessment, error) {
+ rows, err := s.pool.Query(ctx, `
+ SELECT
+ id, hazard_id, version, assessment_type,
+ severity, exposure, probability,
+ inherent_risk, control_maturity, control_coverage,
+ test_evidence_strength, c_eff, residual_risk,
+ risk_level, is_acceptable, acceptance_justification,
+ assessed_by, created_at
+ FROM iace_risk_assessments
+ WHERE hazard_id = $1
+ ORDER BY version DESC, created_at DESC
+ `, hazardID)
+ if err != nil {
+ return nil, fmt.Errorf("list assessments: %w", err)
+ }
+ defer rows.Close()
+
+ var assessments []RiskAssessment
+ for rows.Next() {
+ var a RiskAssessment
+ var assessmentType, riskLevel string
+
+ err := rows.Scan(
+ &a.ID, &a.HazardID, &a.Version, &assessmentType,
+ &a.Severity, &a.Exposure, &a.Probability,
+ &a.InherentRisk, &a.ControlMaturity, &a.ControlCoverage,
+ &a.TestEvidenceStrength, &a.CEff, &a.ResidualRisk,
+ &riskLevel, &a.IsAcceptable, &a.AcceptanceJustification,
+ &a.AssessedBy, &a.CreatedAt,
+ )
+ if err != nil {
+ return nil, fmt.Errorf("list assessments scan: %w", err)
+ }
+
+ a.AssessmentType = AssessmentType(assessmentType)
+ a.RiskLevel = RiskLevel(riskLevel)
+
+ assessments = append(assessments, a)
+ }
+
+ return assessments, nil
+}
+
+// ============================================================================
+// Risk Summary (Aggregated View)
+// ============================================================================
+
+// GetRiskSummary computes an aggregated risk overview for a project
+func (s *Store) GetRiskSummary(ctx context.Context, projectID uuid.UUID) (*RiskSummaryResponse, error) {
+ // Get all hazards for the project
+ hazards, err := s.ListHazards(ctx, projectID)
+ if err != nil {
+ return nil, fmt.Errorf("get risk summary - list hazards: %w", err)
+ }
+
+ summary := &RiskSummaryResponse{
+ TotalHazards: len(hazards),
+ AllAcceptable: true,
+ }
+
+ if len(hazards) == 0 {
+ summary.OverallRiskLevel = RiskLevelNegligible
+ return summary, nil
+ }
+
+ highestRisk := RiskLevelNegligible
+
+ for _, h := range hazards {
+ latest, err := s.GetLatestAssessment(ctx, h.ID)
+ if err != nil {
+ return nil, fmt.Errorf("get risk summary - get assessment for hazard %s: %w", h.ID, err)
+ }
+ if latest == nil {
+ // Hazard without assessment counts as unassessed; consider it not acceptable
+ summary.AllAcceptable = false
+ continue
+ }
+
+ switch latest.RiskLevel {
+ case RiskLevelNotAcceptable:
+ summary.NotAcceptable++
+ case RiskLevelVeryHigh:
+ summary.VeryHigh++
+ case RiskLevelCritical:
+ summary.Critical++
+ case RiskLevelHigh:
+ summary.High++
+ case RiskLevelMedium:
+ summary.Medium++
+ case RiskLevelLow:
+ summary.Low++
+ case RiskLevelNegligible:
+ summary.Negligible++
+ }
+
+ if !latest.IsAcceptable {
+ summary.AllAcceptable = false
+ }
+
+ // Track highest risk level
+ if riskLevelSeverity(latest.RiskLevel) > riskLevelSeverity(highestRisk) {
+ highestRisk = latest.RiskLevel
+ }
+ }
+
+ summary.OverallRiskLevel = highestRisk
+
+ return summary, nil
+}
+
+// riskLevelSeverity returns a numeric severity for risk level comparison
+func riskLevelSeverity(rl RiskLevel) int {
+ switch rl {
+ case RiskLevelNotAcceptable:
+ return 7
+ case RiskLevelVeryHigh:
+ return 6
+ case RiskLevelCritical:
+ return 5
+ case RiskLevelHigh:
+ return 4
+ case RiskLevelMedium:
+ return 3
+ case RiskLevelLow:
+ return 2
+ case RiskLevelNegligible:
+ return 1
+ default:
+ return 0
+ }
+}
+
+// ============================================================================
+// Hazard Library Operations
+// ============================================================================
+
+// ListHazardLibrary lists hazard library entries, optionally filtered by category and component type
+func (s *Store) ListHazardLibrary(ctx context.Context, category string, componentType string) ([]HazardLibraryEntry, error) {
+ query := `
+ SELECT
+ id, category, COALESCE(sub_category, ''), name, description,
+ default_severity, default_probability,
+ COALESCE(default_exposure, 3), COALESCE(default_avoidance, 3),
+ applicable_component_types, regulation_references,
+ suggested_mitigations,
+ COALESCE(typical_causes, '[]'::jsonb),
+ COALESCE(typical_harm, ''),
+ COALESCE(relevant_lifecycle_phases, '[]'::jsonb),
+ COALESCE(recommended_measures_design, '[]'::jsonb),
+ COALESCE(recommended_measures_technical, '[]'::jsonb),
+ COALESCE(recommended_measures_information, '[]'::jsonb),
+ COALESCE(suggested_evidence, '[]'::jsonb),
+ COALESCE(related_keywords, '[]'::jsonb),
+ is_builtin, tenant_id,
+ created_at
+ FROM iace_hazard_library WHERE 1=1`
+
+ args := []interface{}{}
+ argIdx := 1
+
+ if category != "" {
+ query += fmt.Sprintf(" AND category = $%d", argIdx)
+ args = append(args, category)
+ argIdx++
+ }
+ if componentType != "" {
+ query += fmt.Sprintf(" AND applicable_component_types @> $%d::jsonb", argIdx)
+ componentTypeJSON, _ := json.Marshal([]string{componentType})
+ args = append(args, string(componentTypeJSON))
+ argIdx++
+ }
+
+ query += " ORDER BY category ASC, name ASC"
+
+ rows, err := s.pool.Query(ctx, query, args...)
+ if err != nil {
+ return nil, fmt.Errorf("list hazard library: %w", err)
+ }
+ defer rows.Close()
+
+ var entries []HazardLibraryEntry
+ for rows.Next() {
+ var e HazardLibraryEntry
+ var applicableComponentTypes, regulationReferences, suggestedMitigations []byte
+ var typicalCauses, relevantPhases, measuresDesign, measuresTechnical, measuresInfo, evidence, keywords []byte
+
+ err := rows.Scan(
+ &e.ID, &e.Category, &e.SubCategory, &e.Name, &e.Description,
+ &e.DefaultSeverity, &e.DefaultProbability,
+ &e.DefaultExposure, &e.DefaultAvoidance,
+ &applicableComponentTypes, ®ulationReferences,
+ &suggestedMitigations,
+ &typicalCauses, &e.TypicalHarm, &relevantPhases,
+ &measuresDesign, &measuresTechnical, &measuresInfo,
+ &evidence, &keywords,
+ &e.IsBuiltin, &e.TenantID,
+ &e.CreatedAt,
+ )
+ if err != nil {
+ return nil, fmt.Errorf("list hazard library scan: %w", err)
+ }
+
+ json.Unmarshal(applicableComponentTypes, &e.ApplicableComponentTypes)
+ json.Unmarshal(regulationReferences, &e.RegulationReferences)
+ json.Unmarshal(suggestedMitigations, &e.SuggestedMitigations)
+ json.Unmarshal(typicalCauses, &e.TypicalCauses)
+ json.Unmarshal(relevantPhases, &e.RelevantLifecyclePhases)
+ json.Unmarshal(measuresDesign, &e.RecommendedMeasuresDesign)
+ json.Unmarshal(measuresTechnical, &e.RecommendedMeasuresTechnical)
+ json.Unmarshal(measuresInfo, &e.RecommendedMeasuresInformation)
+ json.Unmarshal(evidence, &e.SuggestedEvidence)
+ json.Unmarshal(keywords, &e.RelatedKeywords)
+
+ if e.ApplicableComponentTypes == nil {
+ e.ApplicableComponentTypes = []string{}
+ }
+ if e.RegulationReferences == nil {
+ e.RegulationReferences = []string{}
+ }
+
+ entries = append(entries, e)
+ }
+
+ return entries, nil
+}
+
+// GetHazardLibraryEntry retrieves a single hazard library entry by ID
+func (s *Store) GetHazardLibraryEntry(ctx context.Context, id uuid.UUID) (*HazardLibraryEntry, error) {
+ var e HazardLibraryEntry
+ var applicableComponentTypes, regulationReferences, suggestedMitigations []byte
+ var typicalCauses, relevantLifecyclePhases []byte
+ var recommendedMeasuresDesign, recommendedMeasuresTechnical, recommendedMeasuresInformation []byte
+ var suggestedEvidence, relatedKeywords []byte
+
+ err := s.pool.QueryRow(ctx, `
+ SELECT
+ id, category, name, description,
+ default_severity, default_probability,
+ applicable_component_types, regulation_references,
+ suggested_mitigations, is_builtin, tenant_id,
+ created_at,
+ COALESCE(sub_category, ''),
+ COALESCE(default_exposure, 3),
+ COALESCE(default_avoidance, 3),
+ COALESCE(typical_causes, '[]'),
+ COALESCE(typical_harm, ''),
+ COALESCE(relevant_lifecycle_phases, '[]'),
+ COALESCE(recommended_measures_design, '[]'),
+ COALESCE(recommended_measures_technical, '[]'),
+ COALESCE(recommended_measures_information, '[]'),
+ COALESCE(suggested_evidence, '[]'),
+ COALESCE(related_keywords, '[]')
+ FROM iace_hazard_library WHERE id = $1
+ `, id).Scan(
+ &e.ID, &e.Category, &e.Name, &e.Description,
+ &e.DefaultSeverity, &e.DefaultProbability,
+ &applicableComponentTypes, ®ulationReferences,
+ &suggestedMitigations, &e.IsBuiltin, &e.TenantID,
+ &e.CreatedAt,
+ &e.SubCategory,
+ &e.DefaultExposure, &e.DefaultAvoidance,
+ &typicalCauses, &e.TypicalHarm,
+ &relevantLifecyclePhases,
+ &recommendedMeasuresDesign, &recommendedMeasuresTechnical, &recommendedMeasuresInformation,
+ &suggestedEvidence, &relatedKeywords,
+ )
+ if err == pgx.ErrNoRows {
+ return nil, nil
+ }
+ if err != nil {
+ return nil, fmt.Errorf("get hazard library entry: %w", err)
+ }
+
+ json.Unmarshal(applicableComponentTypes, &e.ApplicableComponentTypes)
+ json.Unmarshal(regulationReferences, &e.RegulationReferences)
+ json.Unmarshal(suggestedMitigations, &e.SuggestedMitigations)
+ json.Unmarshal(typicalCauses, &e.TypicalCauses)
+ json.Unmarshal(relevantLifecyclePhases, &e.RelevantLifecyclePhases)
+ json.Unmarshal(recommendedMeasuresDesign, &e.RecommendedMeasuresDesign)
+ json.Unmarshal(recommendedMeasuresTechnical, &e.RecommendedMeasuresTechnical)
+ json.Unmarshal(recommendedMeasuresInformation, &e.RecommendedMeasuresInformation)
+ json.Unmarshal(suggestedEvidence, &e.SuggestedEvidence)
+ json.Unmarshal(relatedKeywords, &e.RelatedKeywords)
+
+ if e.ApplicableComponentTypes == nil {
+ e.ApplicableComponentTypes = []string{}
+ }
+ if e.RegulationReferences == nil {
+ e.RegulationReferences = []string{}
+ }
+
+ return &e, nil
+}
diff --git a/ai-compliance-sdk/internal/iace/store_mitigations.go b/ai-compliance-sdk/internal/iace/store_mitigations.go
new file mode 100644
index 0000000..08cb70d
--- /dev/null
+++ b/ai-compliance-sdk/internal/iace/store_mitigations.go
@@ -0,0 +1,506 @@
+package iace
+
+import (
+ "context"
+ "fmt"
+ "time"
+
+ "github.com/google/uuid"
+ "github.com/jackc/pgx/v5"
+)
+
+// ============================================================================
+// Mitigation CRUD Operations
+// ============================================================================
+
+// CreateMitigation creates a new mitigation measure for a hazard
+func (s *Store) CreateMitigation(ctx context.Context, req CreateMitigationRequest) (*Mitigation, error) {
+ m := &Mitigation{
+ ID: uuid.New(),
+ HazardID: req.HazardID,
+ ReductionType: req.ReductionType,
+ Name: req.Name,
+ Description: req.Description,
+ Status: MitigationStatusPlanned,
+ CreatedAt: time.Now().UTC(),
+ UpdatedAt: time.Now().UTC(),
+ }
+
+ _, err := s.pool.Exec(ctx, `
+ INSERT INTO iace_mitigations (
+ id, hazard_id, reduction_type, name, description,
+ status, verification_method, verification_result,
+ verified_at, verified_by,
+ created_at, updated_at
+ ) VALUES (
+ $1, $2, $3, $4, $5,
+ $6, $7, $8,
+ $9, $10,
+ $11, $12
+ )
+ `,
+ m.ID, m.HazardID, string(m.ReductionType), m.Name, m.Description,
+ string(m.Status), "", "",
+ nil, uuid.Nil,
+ m.CreatedAt, m.UpdatedAt,
+ )
+ if err != nil {
+ return nil, fmt.Errorf("create mitigation: %w", err)
+ }
+
+ return m, nil
+}
+
+// UpdateMitigation updates a mitigation with a dynamic set of fields
+func (s *Store) UpdateMitigation(ctx context.Context, id uuid.UUID, updates map[string]interface{}) (*Mitigation, error) {
+ if len(updates) == 0 {
+ return s.getMitigation(ctx, id)
+ }
+
+ query := "UPDATE iace_mitigations SET updated_at = NOW()"
+ args := []interface{}{id}
+ argIdx := 2
+
+ for key, val := range updates {
+ switch key {
+ case "name", "description", "verification_result":
+ query += fmt.Sprintf(", %s = $%d", key, argIdx)
+ args = append(args, val)
+ argIdx++
+ case "status":
+ query += fmt.Sprintf(", status = $%d", argIdx)
+ args = append(args, val)
+ argIdx++
+ case "reduction_type":
+ query += fmt.Sprintf(", reduction_type = $%d", argIdx)
+ args = append(args, val)
+ argIdx++
+ case "verification_method":
+ query += fmt.Sprintf(", verification_method = $%d", argIdx)
+ args = append(args, val)
+ argIdx++
+ }
+ }
+
+ query += " WHERE id = $1"
+
+ _, err := s.pool.Exec(ctx, query, args...)
+ if err != nil {
+ return nil, fmt.Errorf("update mitigation: %w", err)
+ }
+
+ return s.getMitigation(ctx, id)
+}
+
+// VerifyMitigation marks a mitigation as verified
+func (s *Store) VerifyMitigation(ctx context.Context, id uuid.UUID, verificationResult string, verifiedBy string) error {
+ now := time.Now().UTC()
+ verifiedByUUID, err := uuid.Parse(verifiedBy)
+ if err != nil {
+ return fmt.Errorf("invalid verified_by UUID: %w", err)
+ }
+
+ _, err = s.pool.Exec(ctx, `
+ UPDATE iace_mitigations SET
+ status = $2,
+ verification_result = $3,
+ verified_at = $4,
+ verified_by = $5,
+ updated_at = $4
+ WHERE id = $1
+ `, id, string(MitigationStatusVerified), verificationResult, now, verifiedByUUID)
+ if err != nil {
+ return fmt.Errorf("verify mitigation: %w", err)
+ }
+
+ return nil
+}
+
+// ListMitigations lists all mitigations for a hazard
+func (s *Store) ListMitigations(ctx context.Context, hazardID uuid.UUID) ([]Mitigation, error) {
+ rows, err := s.pool.Query(ctx, `
+ SELECT
+ id, hazard_id, reduction_type, name, description,
+ status, verification_method, verification_result,
+ verified_at, verified_by,
+ created_at, updated_at
+ FROM iace_mitigations WHERE hazard_id = $1
+ ORDER BY created_at ASC
+ `, hazardID)
+ if err != nil {
+ return nil, fmt.Errorf("list mitigations: %w", err)
+ }
+ defer rows.Close()
+
+ var mitigations []Mitigation
+ for rows.Next() {
+ var m Mitigation
+ var reductionType, status, verificationMethod string
+
+ err := rows.Scan(
+ &m.ID, &m.HazardID, &reductionType, &m.Name, &m.Description,
+ &status, &verificationMethod, &m.VerificationResult,
+ &m.VerifiedAt, &m.VerifiedBy,
+ &m.CreatedAt, &m.UpdatedAt,
+ )
+ if err != nil {
+ return nil, fmt.Errorf("list mitigations scan: %w", err)
+ }
+
+ m.ReductionType = ReductionType(reductionType)
+ m.Status = MitigationStatus(status)
+ m.VerificationMethod = VerificationMethod(verificationMethod)
+
+ mitigations = append(mitigations, m)
+ }
+
+ return mitigations, nil
+}
+
+// GetMitigation fetches a single mitigation by ID.
+func (s *Store) GetMitigation(ctx context.Context, id uuid.UUID) (*Mitigation, error) {
+ return s.getMitigation(ctx, id)
+}
+
+// getMitigation is a helper to fetch a single mitigation by ID
+func (s *Store) getMitigation(ctx context.Context, id uuid.UUID) (*Mitigation, error) {
+ var m Mitigation
+ var reductionType, status, verificationMethod string
+
+ err := s.pool.QueryRow(ctx, `
+ SELECT
+ id, hazard_id, reduction_type, name, description,
+ status, verification_method, verification_result,
+ verified_at, verified_by,
+ created_at, updated_at
+ FROM iace_mitigations WHERE id = $1
+ `, id).Scan(
+ &m.ID, &m.HazardID, &reductionType, &m.Name, &m.Description,
+ &status, &verificationMethod, &m.VerificationResult,
+ &m.VerifiedAt, &m.VerifiedBy,
+ &m.CreatedAt, &m.UpdatedAt,
+ )
+ if err == pgx.ErrNoRows {
+ return nil, nil
+ }
+ if err != nil {
+ return nil, fmt.Errorf("get mitigation: %w", err)
+ }
+
+ m.ReductionType = ReductionType(reductionType)
+ m.Status = MitigationStatus(status)
+ m.VerificationMethod = VerificationMethod(verificationMethod)
+
+ return &m, nil
+}
+
+// ============================================================================
+// Evidence Operations
+// ============================================================================
+
+// CreateEvidence creates a new evidence record
+func (s *Store) CreateEvidence(ctx context.Context, evidence *Evidence) error {
+ if evidence.ID == uuid.Nil {
+ evidence.ID = uuid.New()
+ }
+ if evidence.CreatedAt.IsZero() {
+ evidence.CreatedAt = time.Now().UTC()
+ }
+
+ _, err := s.pool.Exec(ctx, `
+ INSERT INTO iace_evidence (
+ id, project_id, mitigation_id, verification_plan_id,
+ file_name, file_path, file_hash, file_size, mime_type,
+ description, uploaded_by, created_at
+ ) VALUES (
+ $1, $2, $3, $4,
+ $5, $6, $7, $8, $9,
+ $10, $11, $12
+ )
+ `,
+ evidence.ID, evidence.ProjectID, evidence.MitigationID, evidence.VerificationPlanID,
+ evidence.FileName, evidence.FilePath, evidence.FileHash, evidence.FileSize, evidence.MimeType,
+ evidence.Description, evidence.UploadedBy, evidence.CreatedAt,
+ )
+ if err != nil {
+ return fmt.Errorf("create evidence: %w", err)
+ }
+
+ return nil
+}
+
+// ListEvidence lists all evidence for a project
+func (s *Store) ListEvidence(ctx context.Context, projectID uuid.UUID) ([]Evidence, error) {
+ rows, err := s.pool.Query(ctx, `
+ SELECT
+ id, project_id, mitigation_id, verification_plan_id,
+ file_name, file_path, file_hash, file_size, mime_type,
+ description, uploaded_by, created_at
+ FROM iace_evidence WHERE project_id = $1
+ ORDER BY created_at DESC
+ `, projectID)
+ if err != nil {
+ return nil, fmt.Errorf("list evidence: %w", err)
+ }
+ defer rows.Close()
+
+ var evidence []Evidence
+ for rows.Next() {
+ var e Evidence
+
+ err := rows.Scan(
+ &e.ID, &e.ProjectID, &e.MitigationID, &e.VerificationPlanID,
+ &e.FileName, &e.FilePath, &e.FileHash, &e.FileSize, &e.MimeType,
+ &e.Description, &e.UploadedBy, &e.CreatedAt,
+ )
+ if err != nil {
+ return nil, fmt.Errorf("list evidence scan: %w", err)
+ }
+
+ evidence = append(evidence, e)
+ }
+
+ return evidence, nil
+}
+
+// ============================================================================
+// Verification Plan Operations
+// ============================================================================
+
+// CreateVerificationPlan creates a new verification plan
+func (s *Store) CreateVerificationPlan(ctx context.Context, req CreateVerificationPlanRequest) (*VerificationPlan, error) {
+ vp := &VerificationPlan{
+ ID: uuid.New(),
+ ProjectID: req.ProjectID,
+ HazardID: req.HazardID,
+ MitigationID: req.MitigationID,
+ Title: req.Title,
+ Description: req.Description,
+ AcceptanceCriteria: req.AcceptanceCriteria,
+ Method: req.Method,
+ Status: "planned",
+ CreatedAt: time.Now().UTC(),
+ UpdatedAt: time.Now().UTC(),
+ }
+
+ _, err := s.pool.Exec(ctx, `
+ INSERT INTO iace_verification_plans (
+ id, project_id, hazard_id, mitigation_id,
+ title, description, acceptance_criteria, method,
+ status, result, completed_at, completed_by,
+ created_at, updated_at
+ ) VALUES (
+ $1, $2, $3, $4,
+ $5, $6, $7, $8,
+ $9, $10, $11, $12,
+ $13, $14
+ )
+ `,
+ vp.ID, vp.ProjectID, vp.HazardID, vp.MitigationID,
+ vp.Title, vp.Description, vp.AcceptanceCriteria, string(vp.Method),
+ vp.Status, "", nil, uuid.Nil,
+ vp.CreatedAt, vp.UpdatedAt,
+ )
+ if err != nil {
+ return nil, fmt.Errorf("create verification plan: %w", err)
+ }
+
+ return vp, nil
+}
+
+// UpdateVerificationPlan updates a verification plan with a dynamic set of fields
+func (s *Store) UpdateVerificationPlan(ctx context.Context, id uuid.UUID, updates map[string]interface{}) (*VerificationPlan, error) {
+ if len(updates) == 0 {
+ return s.getVerificationPlan(ctx, id)
+ }
+
+ query := "UPDATE iace_verification_plans SET updated_at = NOW()"
+ args := []interface{}{id}
+ argIdx := 2
+
+ for key, val := range updates {
+ switch key {
+ case "title", "description", "acceptance_criteria", "result", "status":
+ query += fmt.Sprintf(", %s = $%d", key, argIdx)
+ args = append(args, val)
+ argIdx++
+ case "method":
+ query += fmt.Sprintf(", method = $%d", argIdx)
+ args = append(args, val)
+ argIdx++
+ }
+ }
+
+ query += " WHERE id = $1"
+
+ _, err := s.pool.Exec(ctx, query, args...)
+ if err != nil {
+ return nil, fmt.Errorf("update verification plan: %w", err)
+ }
+
+ return s.getVerificationPlan(ctx, id)
+}
+
+// CompleteVerification marks a verification plan as completed
+func (s *Store) CompleteVerification(ctx context.Context, id uuid.UUID, result string, completedBy string) error {
+ now := time.Now().UTC()
+ completedByUUID, err := uuid.Parse(completedBy)
+ if err != nil {
+ return fmt.Errorf("invalid completed_by UUID: %w", err)
+ }
+
+ _, err = s.pool.Exec(ctx, `
+ UPDATE iace_verification_plans SET
+ status = 'completed',
+ result = $2,
+ completed_at = $3,
+ completed_by = $4,
+ updated_at = $3
+ WHERE id = $1
+ `, id, result, now, completedByUUID)
+ if err != nil {
+ return fmt.Errorf("complete verification: %w", err)
+ }
+
+ return nil
+}
+
+// ListVerificationPlans lists all verification plans for a project
+func (s *Store) ListVerificationPlans(ctx context.Context, projectID uuid.UUID) ([]VerificationPlan, error) {
+ rows, err := s.pool.Query(ctx, `
+ SELECT
+ id, project_id, hazard_id, mitigation_id,
+ title, description, acceptance_criteria, method,
+ status, result, completed_at, completed_by,
+ created_at, updated_at
+ FROM iace_verification_plans WHERE project_id = $1
+ ORDER BY created_at ASC
+ `, projectID)
+ if err != nil {
+ return nil, fmt.Errorf("list verification plans: %w", err)
+ }
+ defer rows.Close()
+
+ var plans []VerificationPlan
+ for rows.Next() {
+ var vp VerificationPlan
+ var method string
+
+ err := rows.Scan(
+ &vp.ID, &vp.ProjectID, &vp.HazardID, &vp.MitigationID,
+ &vp.Title, &vp.Description, &vp.AcceptanceCriteria, &method,
+ &vp.Status, &vp.Result, &vp.CompletedAt, &vp.CompletedBy,
+ &vp.CreatedAt, &vp.UpdatedAt,
+ )
+ if err != nil {
+ return nil, fmt.Errorf("list verification plans scan: %w", err)
+ }
+
+ vp.Method = VerificationMethod(method)
+ plans = append(plans, vp)
+ }
+
+ return plans, nil
+}
+
+// getVerificationPlan is a helper to fetch a single verification plan by ID
+func (s *Store) getVerificationPlan(ctx context.Context, id uuid.UUID) (*VerificationPlan, error) {
+ var vp VerificationPlan
+ var method string
+
+ err := s.pool.QueryRow(ctx, `
+ SELECT
+ id, project_id, hazard_id, mitigation_id,
+ title, description, acceptance_criteria, method,
+ status, result, completed_at, completed_by,
+ created_at, updated_at
+ FROM iace_verification_plans WHERE id = $1
+ `, id).Scan(
+ &vp.ID, &vp.ProjectID, &vp.HazardID, &vp.MitigationID,
+ &vp.Title, &vp.Description, &vp.AcceptanceCriteria, &method,
+ &vp.Status, &vp.Result, &vp.CompletedAt, &vp.CompletedBy,
+ &vp.CreatedAt, &vp.UpdatedAt,
+ )
+ if err == pgx.ErrNoRows {
+ return nil, nil
+ }
+ if err != nil {
+ return nil, fmt.Errorf("get verification plan: %w", err)
+ }
+
+ vp.Method = VerificationMethod(method)
+ return &vp, nil
+}
+
+// ============================================================================
+// Reference Data Operations
+// ============================================================================
+
+// ListLifecyclePhases returns all 12 lifecycle phases with DE/EN labels
+func (s *Store) ListLifecyclePhases(ctx context.Context) ([]LifecyclePhaseInfo, error) {
+ rows, err := s.pool.Query(ctx, `
+ SELECT id, label_de, label_en, sort_order
+ FROM iace_lifecycle_phases
+ ORDER BY sort_order ASC
+ `)
+ if err != nil {
+ return nil, fmt.Errorf("list lifecycle phases: %w", err)
+ }
+ defer rows.Close()
+
+ var phases []LifecyclePhaseInfo
+ for rows.Next() {
+ var p LifecyclePhaseInfo
+ if err := rows.Scan(&p.ID, &p.LabelDE, &p.LabelEN, &p.Sort); err != nil {
+ return nil, fmt.Errorf("list lifecycle phases scan: %w", err)
+ }
+ phases = append(phases, p)
+ }
+ return phases, nil
+}
+
+// ListRoles returns all affected person roles from the reference table
+func (s *Store) ListRoles(ctx context.Context) ([]RoleInfo, error) {
+ rows, err := s.pool.Query(ctx, `
+ SELECT id, label_de, label_en, sort_order
+ FROM iace_roles
+ ORDER BY sort_order ASC
+ `)
+ if err != nil {
+ return nil, fmt.Errorf("list roles: %w", err)
+ }
+ defer rows.Close()
+
+ var roles []RoleInfo
+ for rows.Next() {
+ var r RoleInfo
+ if err := rows.Scan(&r.ID, &r.LabelDE, &r.LabelEN, &r.Sort); err != nil {
+ return nil, fmt.Errorf("list roles scan: %w", err)
+ }
+ roles = append(roles, r)
+ }
+ return roles, nil
+}
+
+// ListEvidenceTypes returns all evidence types from the reference table
+func (s *Store) ListEvidenceTypes(ctx context.Context) ([]EvidenceTypeInfo, error) {
+ rows, err := s.pool.Query(ctx, `
+ SELECT id, category, label_de, label_en, sort_order
+ FROM iace_evidence_types
+ ORDER BY sort_order ASC
+ `)
+ if err != nil {
+ return nil, fmt.Errorf("list evidence types: %w", err)
+ }
+ defer rows.Close()
+
+ var types []EvidenceTypeInfo
+ for rows.Next() {
+ var e EvidenceTypeInfo
+ if err := rows.Scan(&e.ID, &e.Category, &e.LabelDE, &e.LabelEN, &e.Sort); err != nil {
+ return nil, fmt.Errorf("list evidence types scan: %w", err)
+ }
+ types = append(types, e)
+ }
+ return types, nil
+}
diff --git a/ai-compliance-sdk/internal/iace/store_projects.go b/ai-compliance-sdk/internal/iace/store_projects.go
new file mode 100644
index 0000000..cd2860c
--- /dev/null
+++ b/ai-compliance-sdk/internal/iace/store_projects.go
@@ -0,0 +1,529 @@
+package iace
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "time"
+
+ "github.com/google/uuid"
+ "github.com/jackc/pgx/v5"
+)
+
+// ============================================================================
+// Project CRUD Operations
+// ============================================================================
+
+// CreateProject creates a new IACE project
+func (s *Store) CreateProject(ctx context.Context, tenantID uuid.UUID, req CreateProjectRequest) (*Project, error) {
+ project := &Project{
+ ID: uuid.New(),
+ TenantID: tenantID,
+ MachineName: req.MachineName,
+ MachineType: req.MachineType,
+ Manufacturer: req.Manufacturer,
+ Description: req.Description,
+ NarrativeText: req.NarrativeText,
+ Status: ProjectStatusDraft,
+ CEMarkingTarget: req.CEMarkingTarget,
+ Metadata: req.Metadata,
+ CreatedAt: time.Now().UTC(),
+ UpdatedAt: time.Now().UTC(),
+ }
+
+ _, err := s.pool.Exec(ctx, `
+ INSERT INTO iace_projects (
+ id, tenant_id, machine_name, machine_type, manufacturer,
+ description, narrative_text, status, ce_marking_target,
+ completeness_score, risk_summary, triggered_regulations, metadata,
+ created_at, updated_at, archived_at
+ ) VALUES (
+ $1, $2, $3, $4, $5,
+ $6, $7, $8, $9,
+ $10, $11, $12, $13,
+ $14, $15, $16
+ )
+ `,
+ project.ID, project.TenantID, project.MachineName, project.MachineType, project.Manufacturer,
+ project.Description, project.NarrativeText, string(project.Status), project.CEMarkingTarget,
+ project.CompletenessScore, nil, project.TriggeredRegulations, project.Metadata,
+ project.CreatedAt, project.UpdatedAt, project.ArchivedAt,
+ )
+ if err != nil {
+ return nil, fmt.Errorf("create project: %w", err)
+ }
+
+ return project, nil
+}
+
+// GetProject retrieves a project by ID
+func (s *Store) GetProject(ctx context.Context, id uuid.UUID) (*Project, error) {
+ var p Project
+ var status string
+ var riskSummary, triggeredRegulations, metadata []byte
+
+ err := s.pool.QueryRow(ctx, `
+ SELECT
+ id, tenant_id, machine_name, machine_type, manufacturer,
+ description, narrative_text, status, ce_marking_target,
+ completeness_score, risk_summary, triggered_regulations, metadata,
+ created_at, updated_at, archived_at
+ FROM iace_projects WHERE id = $1
+ `, id).Scan(
+ &p.ID, &p.TenantID, &p.MachineName, &p.MachineType, &p.Manufacturer,
+ &p.Description, &p.NarrativeText, &status, &p.CEMarkingTarget,
+ &p.CompletenessScore, &riskSummary, &triggeredRegulations, &metadata,
+ &p.CreatedAt, &p.UpdatedAt, &p.ArchivedAt,
+ )
+ if err == pgx.ErrNoRows {
+ return nil, nil
+ }
+ if err != nil {
+ return nil, fmt.Errorf("get project: %w", err)
+ }
+
+ p.Status = ProjectStatus(status)
+ json.Unmarshal(riskSummary, &p.RiskSummary)
+ json.Unmarshal(triggeredRegulations, &p.TriggeredRegulations)
+ json.Unmarshal(metadata, &p.Metadata)
+
+ return &p, nil
+}
+
+// ListProjects lists all projects for a tenant
+func (s *Store) ListProjects(ctx context.Context, tenantID uuid.UUID) ([]Project, error) {
+ rows, err := s.pool.Query(ctx, `
+ SELECT
+ id, tenant_id, machine_name, machine_type, manufacturer,
+ description, narrative_text, status, ce_marking_target,
+ completeness_score, risk_summary, triggered_regulations, metadata,
+ created_at, updated_at, archived_at
+ FROM iace_projects WHERE tenant_id = $1
+ ORDER BY created_at DESC
+ `, tenantID)
+ if err != nil {
+ return nil, fmt.Errorf("list projects: %w", err)
+ }
+ defer rows.Close()
+
+ var projects []Project
+ for rows.Next() {
+ var p Project
+ var status string
+ var riskSummary, triggeredRegulations, metadata []byte
+
+ err := rows.Scan(
+ &p.ID, &p.TenantID, &p.MachineName, &p.MachineType, &p.Manufacturer,
+ &p.Description, &p.NarrativeText, &status, &p.CEMarkingTarget,
+ &p.CompletenessScore, &riskSummary, &triggeredRegulations, &metadata,
+ &p.CreatedAt, &p.UpdatedAt, &p.ArchivedAt,
+ )
+ if err != nil {
+ return nil, fmt.Errorf("list projects scan: %w", err)
+ }
+
+ p.Status = ProjectStatus(status)
+ json.Unmarshal(riskSummary, &p.RiskSummary)
+ json.Unmarshal(triggeredRegulations, &p.TriggeredRegulations)
+ json.Unmarshal(metadata, &p.Metadata)
+
+ projects = append(projects, p)
+ }
+
+ return projects, nil
+}
+
+// UpdateProject updates an existing project's mutable fields
+func (s *Store) UpdateProject(ctx context.Context, id uuid.UUID, req UpdateProjectRequest) (*Project, error) {
+ // Fetch current project first
+ project, err := s.GetProject(ctx, id)
+ if err != nil {
+ return nil, err
+ }
+ if project == nil {
+ return nil, nil
+ }
+
+ // Apply partial updates
+ if req.MachineName != nil {
+ project.MachineName = *req.MachineName
+ }
+ if req.MachineType != nil {
+ project.MachineType = *req.MachineType
+ }
+ if req.Manufacturer != nil {
+ project.Manufacturer = *req.Manufacturer
+ }
+ if req.Description != nil {
+ project.Description = *req.Description
+ }
+ if req.NarrativeText != nil {
+ project.NarrativeText = *req.NarrativeText
+ }
+ if req.CEMarkingTarget != nil {
+ project.CEMarkingTarget = *req.CEMarkingTarget
+ }
+ if req.Metadata != nil {
+ project.Metadata = *req.Metadata
+ }
+
+ project.UpdatedAt = time.Now().UTC()
+
+ _, err = s.pool.Exec(ctx, `
+ UPDATE iace_projects SET
+ machine_name = $2, machine_type = $3, manufacturer = $4,
+ description = $5, narrative_text = $6, ce_marking_target = $7,
+ metadata = $8, updated_at = $9
+ WHERE id = $1
+ `,
+ id, project.MachineName, project.MachineType, project.Manufacturer,
+ project.Description, project.NarrativeText, project.CEMarkingTarget,
+ project.Metadata, project.UpdatedAt,
+ )
+ if err != nil {
+ return nil, fmt.Errorf("update project: %w", err)
+ }
+
+ return project, nil
+}
+
+// ArchiveProject sets the archived_at timestamp and status for a project
+func (s *Store) ArchiveProject(ctx context.Context, id uuid.UUID) error {
+ now := time.Now().UTC()
+ _, err := s.pool.Exec(ctx, `
+ UPDATE iace_projects SET
+ status = $2, archived_at = $3, updated_at = $3
+ WHERE id = $1
+ `, id, string(ProjectStatusArchived), now)
+ if err != nil {
+ return fmt.Errorf("archive project: %w", err)
+ }
+ return nil
+}
+
+// UpdateProjectStatus updates the lifecycle status of a project
+func (s *Store) UpdateProjectStatus(ctx context.Context, id uuid.UUID, status ProjectStatus) error {
+ _, err := s.pool.Exec(ctx, `
+ UPDATE iace_projects SET status = $2, updated_at = NOW()
+ WHERE id = $1
+ `, id, string(status))
+ if err != nil {
+ return fmt.Errorf("update project status: %w", err)
+ }
+ return nil
+}
+
+// UpdateProjectCompleteness updates the completeness score and risk summary
+func (s *Store) UpdateProjectCompleteness(ctx context.Context, id uuid.UUID, score float64, riskSummary map[string]int) error {
+ riskSummaryJSON, err := json.Marshal(riskSummary)
+ if err != nil {
+ return fmt.Errorf("marshal risk summary: %w", err)
+ }
+
+ _, err = s.pool.Exec(ctx, `
+ UPDATE iace_projects SET
+ completeness_score = $2, risk_summary = $3, updated_at = NOW()
+ WHERE id = $1
+ `, id, score, riskSummaryJSON)
+ if err != nil {
+ return fmt.Errorf("update project completeness: %w", err)
+ }
+ return nil
+}
+
+// ============================================================================
+// Component CRUD Operations
+// ============================================================================
+
+// CreateComponent creates a new component within a project
+func (s *Store) CreateComponent(ctx context.Context, req CreateComponentRequest) (*Component, error) {
+ comp := &Component{
+ ID: uuid.New(),
+ ProjectID: req.ProjectID,
+ ParentID: req.ParentID,
+ Name: req.Name,
+ ComponentType: req.ComponentType,
+ Version: req.Version,
+ Description: req.Description,
+ IsSafetyRelevant: req.IsSafetyRelevant,
+ IsNetworked: req.IsNetworked,
+ CreatedAt: time.Now().UTC(),
+ UpdatedAt: time.Now().UTC(),
+ }
+
+ _, err := s.pool.Exec(ctx, `
+ INSERT INTO iace_components (
+ id, project_id, parent_id, name, component_type,
+ version, description, is_safety_relevant, is_networked,
+ metadata, sort_order, created_at, updated_at
+ ) VALUES (
+ $1, $2, $3, $4, $5,
+ $6, $7, $8, $9,
+ $10, $11, $12, $13
+ )
+ `,
+ comp.ID, comp.ProjectID, comp.ParentID, comp.Name, string(comp.ComponentType),
+ comp.Version, comp.Description, comp.IsSafetyRelevant, comp.IsNetworked,
+ comp.Metadata, comp.SortOrder, comp.CreatedAt, comp.UpdatedAt,
+ )
+ if err != nil {
+ return nil, fmt.Errorf("create component: %w", err)
+ }
+
+ return comp, nil
+}
+
+// GetComponent retrieves a component by ID
+func (s *Store) GetComponent(ctx context.Context, id uuid.UUID) (*Component, error) {
+ var c Component
+ var compType string
+ var metadata []byte
+
+ err := s.pool.QueryRow(ctx, `
+ SELECT
+ id, project_id, parent_id, name, component_type,
+ version, description, is_safety_relevant, is_networked,
+ metadata, sort_order, created_at, updated_at
+ FROM iace_components WHERE id = $1
+ `, id).Scan(
+ &c.ID, &c.ProjectID, &c.ParentID, &c.Name, &compType,
+ &c.Version, &c.Description, &c.IsSafetyRelevant, &c.IsNetworked,
+ &metadata, &c.SortOrder, &c.CreatedAt, &c.UpdatedAt,
+ )
+ if err == pgx.ErrNoRows {
+ return nil, nil
+ }
+ if err != nil {
+ return nil, fmt.Errorf("get component: %w", err)
+ }
+
+ c.ComponentType = ComponentType(compType)
+ json.Unmarshal(metadata, &c.Metadata)
+
+ return &c, nil
+}
+
+// ListComponents lists all components for a project
+func (s *Store) ListComponents(ctx context.Context, projectID uuid.UUID) ([]Component, error) {
+ rows, err := s.pool.Query(ctx, `
+ SELECT
+ id, project_id, parent_id, name, component_type,
+ version, description, is_safety_relevant, is_networked,
+ metadata, sort_order, created_at, updated_at
+ FROM iace_components WHERE project_id = $1
+ ORDER BY sort_order ASC, created_at ASC
+ `, projectID)
+ if err != nil {
+ return nil, fmt.Errorf("list components: %w", err)
+ }
+ defer rows.Close()
+
+ var components []Component
+ for rows.Next() {
+ var c Component
+ var compType string
+ var metadata []byte
+
+ err := rows.Scan(
+ &c.ID, &c.ProjectID, &c.ParentID, &c.Name, &compType,
+ &c.Version, &c.Description, &c.IsSafetyRelevant, &c.IsNetworked,
+ &metadata, &c.SortOrder, &c.CreatedAt, &c.UpdatedAt,
+ )
+ if err != nil {
+ return nil, fmt.Errorf("list components scan: %w", err)
+ }
+
+ c.ComponentType = ComponentType(compType)
+ json.Unmarshal(metadata, &c.Metadata)
+
+ components = append(components, c)
+ }
+
+ return components, nil
+}
+
+// UpdateComponent updates a component with a dynamic set of fields
+func (s *Store) UpdateComponent(ctx context.Context, id uuid.UUID, updates map[string]interface{}) (*Component, error) {
+ if len(updates) == 0 {
+ return s.GetComponent(ctx, id)
+ }
+
+ query := "UPDATE iace_components SET updated_at = NOW()"
+ args := []interface{}{id}
+ argIdx := 2
+
+ for key, val := range updates {
+ switch key {
+ case "name", "version", "description":
+ query += fmt.Sprintf(", %s = $%d", key, argIdx)
+ args = append(args, val)
+ argIdx++
+ case "component_type":
+ query += fmt.Sprintf(", component_type = $%d", argIdx)
+ args = append(args, val)
+ argIdx++
+ case "is_safety_relevant":
+ query += fmt.Sprintf(", is_safety_relevant = $%d", argIdx)
+ args = append(args, val)
+ argIdx++
+ case "is_networked":
+ query += fmt.Sprintf(", is_networked = $%d", argIdx)
+ args = append(args, val)
+ argIdx++
+ case "sort_order":
+ query += fmt.Sprintf(", sort_order = $%d", argIdx)
+ args = append(args, val)
+ argIdx++
+ case "metadata":
+ metaJSON, _ := json.Marshal(val)
+ query += fmt.Sprintf(", metadata = $%d", argIdx)
+ args = append(args, metaJSON)
+ argIdx++
+ case "parent_id":
+ query += fmt.Sprintf(", parent_id = $%d", argIdx)
+ args = append(args, val)
+ argIdx++
+ }
+ }
+
+ query += " WHERE id = $1"
+
+ _, err := s.pool.Exec(ctx, query, args...)
+ if err != nil {
+ return nil, fmt.Errorf("update component: %w", err)
+ }
+
+ return s.GetComponent(ctx, id)
+}
+
+// DeleteComponent deletes a component by ID
+func (s *Store) DeleteComponent(ctx context.Context, id uuid.UUID) error {
+ _, err := s.pool.Exec(ctx, "DELETE FROM iace_components WHERE id = $1", id)
+ if err != nil {
+ return fmt.Errorf("delete component: %w", err)
+ }
+ return nil
+}
+
+// ============================================================================
+// Classification Operations
+// ============================================================================
+
+// UpsertClassification inserts or updates a regulatory classification for a project
+func (s *Store) UpsertClassification(ctx context.Context, projectID uuid.UUID, regulation RegulationType, result string, riskLevel string, confidence float64, reasoning string, ragSources, requirements json.RawMessage) (*RegulatoryClassification, error) {
+ id := uuid.New()
+ now := time.Now().UTC()
+
+ _, err := s.pool.Exec(ctx, `
+ INSERT INTO iace_classifications (
+ id, project_id, regulation, classification_result,
+ risk_level, confidence, reasoning,
+ rag_sources, requirements,
+ created_at, updated_at
+ ) VALUES (
+ $1, $2, $3, $4,
+ $5, $6, $7,
+ $8, $9,
+ $10, $11
+ )
+ ON CONFLICT (project_id, regulation)
+ DO UPDATE SET
+ classification_result = EXCLUDED.classification_result,
+ risk_level = EXCLUDED.risk_level,
+ confidence = EXCLUDED.confidence,
+ reasoning = EXCLUDED.reasoning,
+ rag_sources = EXCLUDED.rag_sources,
+ requirements = EXCLUDED.requirements,
+ updated_at = EXCLUDED.updated_at
+ `,
+ id, projectID, string(regulation), result,
+ riskLevel, confidence, reasoning,
+ ragSources, requirements,
+ now, now,
+ )
+ if err != nil {
+ return nil, fmt.Errorf("upsert classification: %w", err)
+ }
+
+ // Retrieve the upserted row (may have kept the original ID on conflict)
+ return s.getClassificationByProjectAndRegulation(ctx, projectID, regulation)
+}
+
+// getClassificationByProjectAndRegulation is a helper to fetch a single classification
+func (s *Store) getClassificationByProjectAndRegulation(ctx context.Context, projectID uuid.UUID, regulation RegulationType) (*RegulatoryClassification, error) {
+ var c RegulatoryClassification
+ var reg, rl string
+ var ragSources, requirements []byte
+
+ err := s.pool.QueryRow(ctx, `
+ SELECT
+ id, project_id, regulation, classification_result,
+ risk_level, confidence, reasoning,
+ rag_sources, requirements,
+ created_at, updated_at
+ FROM iace_classifications
+ WHERE project_id = $1 AND regulation = $2
+ `, projectID, string(regulation)).Scan(
+ &c.ID, &c.ProjectID, ®, &c.ClassificationResult,
+ &rl, &c.Confidence, &c.Reasoning,
+ &ragSources, &requirements,
+ &c.CreatedAt, &c.UpdatedAt,
+ )
+ if err == pgx.ErrNoRows {
+ return nil, nil
+ }
+ if err != nil {
+ return nil, fmt.Errorf("get classification: %w", err)
+ }
+
+ c.Regulation = RegulationType(reg)
+ c.RiskLevel = RiskLevel(rl)
+ json.Unmarshal(ragSources, &c.RAGSources)
+ json.Unmarshal(requirements, &c.Requirements)
+
+ return &c, nil
+}
+
+// GetClassifications retrieves all classifications for a project
+func (s *Store) GetClassifications(ctx context.Context, projectID uuid.UUID) ([]RegulatoryClassification, error) {
+ rows, err := s.pool.Query(ctx, `
+ SELECT
+ id, project_id, regulation, classification_result,
+ risk_level, confidence, reasoning,
+ rag_sources, requirements,
+ created_at, updated_at
+ FROM iace_classifications
+ WHERE project_id = $1
+ ORDER BY regulation ASC
+ `, projectID)
+ if err != nil {
+ return nil, fmt.Errorf("get classifications: %w", err)
+ }
+ defer rows.Close()
+
+ var classifications []RegulatoryClassification
+ for rows.Next() {
+ var c RegulatoryClassification
+ var reg, rl string
+ var ragSources, requirements []byte
+
+ err := rows.Scan(
+ &c.ID, &c.ProjectID, ®, &c.ClassificationResult,
+ &rl, &c.Confidence, &c.Reasoning,
+ &ragSources, &requirements,
+ &c.CreatedAt, &c.UpdatedAt,
+ )
+ if err != nil {
+ return nil, fmt.Errorf("get classifications scan: %w", err)
+ }
+
+ c.Regulation = RegulationType(reg)
+ c.RiskLevel = RiskLevel(rl)
+ json.Unmarshal(ragSources, &c.RAGSources)
+ json.Unmarshal(requirements, &c.Requirements)
+
+ classifications = append(classifications, c)
+ }
+
+ return classifications, nil
+}
diff --git a/ai-compliance-sdk/internal/training/store.go b/ai-compliance-sdk/internal/training/store.go
index b183d82..51fff13 100644
--- a/ai-compliance-sdk/internal/training/store.go
+++ b/ai-compliance-sdk/internal/training/store.go
@@ -1,13 +1,6 @@
package training
import (
- "context"
- "encoding/json"
- "fmt"
- "time"
-
- "github.com/google/uuid"
- "github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgxpool"
)
@@ -20,1550 +13,3 @@ type Store struct {
func NewStore(pool *pgxpool.Pool) *Store {
return &Store{pool: pool}
}
-
-// ============================================================================
-// Module CRUD Operations
-// ============================================================================
-
-// CreateModule creates a new training module
-func (s *Store) CreateModule(ctx context.Context, module *TrainingModule) error {
- module.ID = uuid.New()
- module.CreatedAt = time.Now().UTC()
- module.UpdatedAt = module.CreatedAt
- if !module.IsActive {
- module.IsActive = true
- }
-
- isoControls, _ := json.Marshal(module.ISOControls)
-
- _, err := s.pool.Exec(ctx, `
- INSERT INTO training_modules (
- id, tenant_id, academy_course_id, module_code, title, description,
- regulation_area, nis2_relevant, iso_controls, frequency_type,
- validity_days, risk_weight, content_type, duration_minutes,
- pass_threshold, is_active, sort_order, created_at, updated_at
- ) VALUES (
- $1, $2, $3, $4, $5, $6,
- $7, $8, $9, $10,
- $11, $12, $13, $14,
- $15, $16, $17, $18, $19
- )
- `,
- module.ID, module.TenantID, module.AcademyCourseID, module.ModuleCode, module.Title, module.Description,
- string(module.RegulationArea), module.NIS2Relevant, isoControls, string(module.FrequencyType),
- module.ValidityDays, module.RiskWeight, module.ContentType, module.DurationMinutes,
- module.PassThreshold, module.IsActive, module.SortOrder, module.CreatedAt, module.UpdatedAt,
- )
-
- return err
-}
-
-// GetModule retrieves a module by ID
-func (s *Store) GetModule(ctx context.Context, id uuid.UUID) (*TrainingModule, error) {
- var module TrainingModule
- var regulationArea, frequencyType string
- var isoControls []byte
-
- err := s.pool.QueryRow(ctx, `
- SELECT
- id, tenant_id, academy_course_id, module_code, title, description,
- regulation_area, nis2_relevant, iso_controls, frequency_type,
- validity_days, risk_weight, content_type, duration_minutes,
- pass_threshold, is_active, sort_order, created_at, updated_at
- FROM training_modules WHERE id = $1
- `, id).Scan(
- &module.ID, &module.TenantID, &module.AcademyCourseID, &module.ModuleCode, &module.Title, &module.Description,
- ®ulationArea, &module.NIS2Relevant, &isoControls, &frequencyType,
- &module.ValidityDays, &module.RiskWeight, &module.ContentType, &module.DurationMinutes,
- &module.PassThreshold, &module.IsActive, &module.SortOrder, &module.CreatedAt, &module.UpdatedAt,
- )
-
- if err == pgx.ErrNoRows {
- return nil, nil
- }
- if err != nil {
- return nil, err
- }
-
- module.RegulationArea = RegulationArea(regulationArea)
- module.FrequencyType = FrequencyType(frequencyType)
- json.Unmarshal(isoControls, &module.ISOControls)
- if module.ISOControls == nil {
- module.ISOControls = []string{}
- }
-
- return &module, nil
-}
-
-// ListModules lists training modules for a tenant with optional filters
-func (s *Store) ListModules(ctx context.Context, tenantID uuid.UUID, filters *ModuleFilters) ([]TrainingModule, int, error) {
- countQuery := "SELECT COUNT(*) FROM training_modules WHERE tenant_id = $1"
- countArgs := []interface{}{tenantID}
- countArgIdx := 2
-
- query := `
- SELECT
- id, tenant_id, academy_course_id, module_code, title, description,
- regulation_area, nis2_relevant, iso_controls, frequency_type,
- validity_days, risk_weight, content_type, duration_minutes,
- pass_threshold, is_active, sort_order, created_at, updated_at
- FROM training_modules WHERE tenant_id = $1`
-
- args := []interface{}{tenantID}
- argIdx := 2
-
- if filters != nil {
- if filters.RegulationArea != "" {
- query += fmt.Sprintf(" AND regulation_area = $%d", argIdx)
- args = append(args, string(filters.RegulationArea))
- argIdx++
- countQuery += fmt.Sprintf(" AND regulation_area = $%d", countArgIdx)
- countArgs = append(countArgs, string(filters.RegulationArea))
- countArgIdx++
- }
- if filters.FrequencyType != "" {
- query += fmt.Sprintf(" AND frequency_type = $%d", argIdx)
- args = append(args, string(filters.FrequencyType))
- argIdx++
- countQuery += fmt.Sprintf(" AND frequency_type = $%d", countArgIdx)
- countArgs = append(countArgs, string(filters.FrequencyType))
- countArgIdx++
- }
- if filters.IsActive != nil {
- query += fmt.Sprintf(" AND is_active = $%d", argIdx)
- args = append(args, *filters.IsActive)
- argIdx++
- countQuery += fmt.Sprintf(" AND is_active = $%d", countArgIdx)
- countArgs = append(countArgs, *filters.IsActive)
- countArgIdx++
- }
- if filters.NIS2Relevant != nil {
- query += fmt.Sprintf(" AND nis2_relevant = $%d", argIdx)
- args = append(args, *filters.NIS2Relevant)
- argIdx++
- countQuery += fmt.Sprintf(" AND nis2_relevant = $%d", countArgIdx)
- countArgs = append(countArgs, *filters.NIS2Relevant)
- countArgIdx++
- }
- if filters.Search != "" {
- query += fmt.Sprintf(" AND (title ILIKE $%d OR description ILIKE $%d OR module_code ILIKE $%d)", argIdx, argIdx, argIdx)
- args = append(args, "%"+filters.Search+"%")
- argIdx++
- countQuery += fmt.Sprintf(" AND (title ILIKE $%d OR description ILIKE $%d OR module_code ILIKE $%d)", countArgIdx, countArgIdx, countArgIdx)
- countArgs = append(countArgs, "%"+filters.Search+"%")
- countArgIdx++
- }
- }
-
- var total int
- err := s.pool.QueryRow(ctx, countQuery, countArgs...).Scan(&total)
- if err != nil {
- return nil, 0, err
- }
-
- query += " ORDER BY sort_order ASC, created_at DESC"
-
- if filters != nil && filters.Limit > 0 {
- query += fmt.Sprintf(" LIMIT $%d", argIdx)
- args = append(args, filters.Limit)
- argIdx++
- if filters.Offset > 0 {
- query += fmt.Sprintf(" OFFSET $%d", argIdx)
- args = append(args, filters.Offset)
- argIdx++
- }
- }
-
- rows, err := s.pool.Query(ctx, query, args...)
- if err != nil {
- return nil, 0, err
- }
- defer rows.Close()
-
- var modules []TrainingModule
- for rows.Next() {
- var module TrainingModule
- var regulationArea, frequencyType string
- var isoControls []byte
-
- err := rows.Scan(
- &module.ID, &module.TenantID, &module.AcademyCourseID, &module.ModuleCode, &module.Title, &module.Description,
- ®ulationArea, &module.NIS2Relevant, &isoControls, &frequencyType,
- &module.ValidityDays, &module.RiskWeight, &module.ContentType, &module.DurationMinutes,
- &module.PassThreshold, &module.IsActive, &module.SortOrder, &module.CreatedAt, &module.UpdatedAt,
- )
- if err != nil {
- return nil, 0, err
- }
-
- module.RegulationArea = RegulationArea(regulationArea)
- module.FrequencyType = FrequencyType(frequencyType)
- json.Unmarshal(isoControls, &module.ISOControls)
- if module.ISOControls == nil {
- module.ISOControls = []string{}
- }
-
- modules = append(modules, module)
- }
-
- if modules == nil {
- modules = []TrainingModule{}
- }
-
- return modules, total, nil
-}
-
-// UpdateModule updates a training module
-func (s *Store) UpdateModule(ctx context.Context, module *TrainingModule) error {
- module.UpdatedAt = time.Now().UTC()
- isoControls, _ := json.Marshal(module.ISOControls)
-
- _, err := s.pool.Exec(ctx, `
- UPDATE training_modules SET
- title = $2, description = $3, nis2_relevant = $4,
- iso_controls = $5, validity_days = $6, risk_weight = $7,
- duration_minutes = $8, pass_threshold = $9, is_active = $10,
- sort_order = $11, updated_at = $12
- WHERE id = $1
- `,
- module.ID, module.Title, module.Description, module.NIS2Relevant,
- isoControls, module.ValidityDays, module.RiskWeight,
- module.DurationMinutes, module.PassThreshold, module.IsActive,
- module.SortOrder, module.UpdatedAt,
- )
-
- return err
-}
-
-// DeleteModule deletes a training module by ID
-func (s *Store) DeleteModule(ctx context.Context, id uuid.UUID) error {
- _, err := s.pool.Exec(ctx, `DELETE FROM training_modules WHERE id = $1`, id)
- return err
-}
-
-// SetAcademyCourseID links a training module to an academy course
-func (s *Store) SetAcademyCourseID(ctx context.Context, moduleID, courseID uuid.UUID) error {
- _, err := s.pool.Exec(ctx, `
- UPDATE training_modules SET academy_course_id = $2, updated_at = $3 WHERE id = $1
- `, moduleID, courseID, time.Now().UTC())
- return err
-}
-
-// ============================================================================
-// Matrix Operations
-// ============================================================================
-
-// GetMatrixForRole returns all matrix entries for a given role
-func (s *Store) GetMatrixForRole(ctx context.Context, tenantID uuid.UUID, roleCode string) ([]TrainingMatrixEntry, error) {
- rows, err := s.pool.Query(ctx, `
- SELECT
- tm.id, tm.tenant_id, tm.role_code, tm.module_id,
- tm.is_mandatory, tm.priority, tm.created_at,
- m.module_code, m.title
- FROM training_matrix tm
- JOIN training_modules m ON m.id = tm.module_id
- WHERE tm.tenant_id = $1 AND tm.role_code = $2
- ORDER BY tm.priority ASC
- `, tenantID, roleCode)
- if err != nil {
- return nil, err
- }
- defer rows.Close()
-
- var entries []TrainingMatrixEntry
- for rows.Next() {
- var entry TrainingMatrixEntry
- err := rows.Scan(
- &entry.ID, &entry.TenantID, &entry.RoleCode, &entry.ModuleID,
- &entry.IsMandatory, &entry.Priority, &entry.CreatedAt,
- &entry.ModuleCode, &entry.ModuleTitle,
- )
- if err != nil {
- return nil, err
- }
- entries = append(entries, entry)
- }
-
- if entries == nil {
- entries = []TrainingMatrixEntry{}
- }
-
- return entries, nil
-}
-
-// GetMatrixForTenant returns the full CTM for a tenant
-func (s *Store) GetMatrixForTenant(ctx context.Context, tenantID uuid.UUID) ([]TrainingMatrixEntry, error) {
- rows, err := s.pool.Query(ctx, `
- SELECT
- tm.id, tm.tenant_id, tm.role_code, tm.module_id,
- tm.is_mandatory, tm.priority, tm.created_at,
- m.module_code, m.title
- FROM training_matrix tm
- JOIN training_modules m ON m.id = tm.module_id
- WHERE tm.tenant_id = $1
- ORDER BY tm.role_code ASC, tm.priority ASC
- `, tenantID)
- if err != nil {
- return nil, err
- }
- defer rows.Close()
-
- var entries []TrainingMatrixEntry
- for rows.Next() {
- var entry TrainingMatrixEntry
- err := rows.Scan(
- &entry.ID, &entry.TenantID, &entry.RoleCode, &entry.ModuleID,
- &entry.IsMandatory, &entry.Priority, &entry.CreatedAt,
- &entry.ModuleCode, &entry.ModuleTitle,
- )
- if err != nil {
- return nil, err
- }
- entries = append(entries, entry)
- }
-
- if entries == nil {
- entries = []TrainingMatrixEntry{}
- }
-
- return entries, nil
-}
-
-// SetMatrixEntry creates or updates a CTM entry
-func (s *Store) SetMatrixEntry(ctx context.Context, entry *TrainingMatrixEntry) error {
- entry.ID = uuid.New()
- entry.CreatedAt = time.Now().UTC()
-
- _, err := s.pool.Exec(ctx, `
- INSERT INTO training_matrix (
- id, tenant_id, role_code, module_id, is_mandatory, priority, created_at
- ) VALUES ($1, $2, $3, $4, $5, $6, $7)
- ON CONFLICT (tenant_id, role_code, module_id)
- DO UPDATE SET is_mandatory = EXCLUDED.is_mandatory, priority = EXCLUDED.priority
- `,
- entry.ID, entry.TenantID, entry.RoleCode, entry.ModuleID,
- entry.IsMandatory, entry.Priority, entry.CreatedAt,
- )
-
- return err
-}
-
-// DeleteMatrixEntry removes a CTM entry
-func (s *Store) DeleteMatrixEntry(ctx context.Context, tenantID uuid.UUID, roleCode string, moduleID uuid.UUID) error {
- _, err := s.pool.Exec(ctx,
- "DELETE FROM training_matrix WHERE tenant_id = $1 AND role_code = $2 AND module_id = $3",
- tenantID, roleCode, moduleID,
- )
- return err
-}
-
-// ============================================================================
-// Assignment Operations
-// ============================================================================
-
-// CreateAssignment creates a new training assignment
-func (s *Store) CreateAssignment(ctx context.Context, assignment *TrainingAssignment) error {
- assignment.ID = uuid.New()
- assignment.CreatedAt = time.Now().UTC()
- assignment.UpdatedAt = assignment.CreatedAt
- if assignment.Status == "" {
- assignment.Status = AssignmentStatusPending
- }
-
- _, err := s.pool.Exec(ctx, `
- INSERT INTO training_assignments (
- id, tenant_id, module_id, user_id, user_name, user_email,
- role_code, trigger_type, trigger_event, status, progress_percent,
- quiz_score, quiz_passed, quiz_attempts,
- started_at, completed_at, deadline, certificate_id,
- escalation_level, last_escalation_at, enrollment_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
- )
- `,
- assignment.ID, assignment.TenantID, assignment.ModuleID, assignment.UserID, assignment.UserName, assignment.UserEmail,
- assignment.RoleCode, string(assignment.TriggerType), assignment.TriggerEvent, string(assignment.Status), assignment.ProgressPercent,
- assignment.QuizScore, assignment.QuizPassed, assignment.QuizAttempts,
- assignment.StartedAt, assignment.CompletedAt, assignment.Deadline, assignment.CertificateID,
- assignment.EscalationLevel, assignment.LastEscalationAt, assignment.EnrollmentID,
- assignment.CreatedAt, assignment.UpdatedAt,
- )
-
- return err
-}
-
-// GetAssignment retrieves an assignment by ID
-func (s *Store) GetAssignment(ctx context.Context, id uuid.UUID) (*TrainingAssignment, error) {
- var a TrainingAssignment
- var status, triggerType string
-
- err := s.pool.QueryRow(ctx, `
- SELECT
- ta.id, ta.tenant_id, ta.module_id, ta.user_id, ta.user_name, ta.user_email,
- ta.role_code, ta.trigger_type, ta.trigger_event, ta.status, ta.progress_percent,
- ta.quiz_score, ta.quiz_passed, ta.quiz_attempts,
- ta.started_at, ta.completed_at, ta.deadline, ta.certificate_id,
- ta.escalation_level, ta.last_escalation_at, ta.enrollment_id,
- ta.created_at, ta.updated_at,
- m.module_code, m.title
- FROM training_assignments ta
- JOIN training_modules m ON m.id = ta.module_id
- WHERE ta.id = $1
- `, id).Scan(
- &a.ID, &a.TenantID, &a.ModuleID, &a.UserID, &a.UserName, &a.UserEmail,
- &a.RoleCode, &triggerType, &a.TriggerEvent, &status, &a.ProgressPercent,
- &a.QuizScore, &a.QuizPassed, &a.QuizAttempts,
- &a.StartedAt, &a.CompletedAt, &a.Deadline, &a.CertificateID,
- &a.EscalationLevel, &a.LastEscalationAt, &a.EnrollmentID,
- &a.CreatedAt, &a.UpdatedAt,
- &a.ModuleCode, &a.ModuleTitle,
- )
-
- if err == pgx.ErrNoRows {
- return nil, nil
- }
- if err != nil {
- return nil, err
- }
-
- a.Status = AssignmentStatus(status)
- a.TriggerType = TriggerType(triggerType)
- return &a, nil
-}
-
-// ListAssignments lists assignments for a tenant with optional filters
-func (s *Store) ListAssignments(ctx context.Context, tenantID uuid.UUID, filters *AssignmentFilters) ([]TrainingAssignment, int, error) {
- countQuery := "SELECT COUNT(*) FROM training_assignments WHERE tenant_id = $1"
- countArgs := []interface{}{tenantID}
- countArgIdx := 2
-
- query := `
- SELECT
- ta.id, ta.tenant_id, ta.module_id, ta.user_id, ta.user_name, ta.user_email,
- ta.role_code, ta.trigger_type, ta.trigger_event, ta.status, ta.progress_percent,
- ta.quiz_score, ta.quiz_passed, ta.quiz_attempts,
- ta.started_at, ta.completed_at, ta.deadline, ta.certificate_id,
- ta.escalation_level, ta.last_escalation_at, ta.enrollment_id,
- ta.created_at, ta.updated_at,
- m.module_code, m.title
- FROM training_assignments ta
- JOIN training_modules m ON m.id = ta.module_id
- WHERE ta.tenant_id = $1`
-
- args := []interface{}{tenantID}
- argIdx := 2
-
- if filters != nil {
- if filters.ModuleID != nil {
- query += fmt.Sprintf(" AND ta.module_id = $%d", argIdx)
- args = append(args, *filters.ModuleID)
- argIdx++
- countQuery += fmt.Sprintf(" AND module_id = $%d", countArgIdx)
- countArgs = append(countArgs, *filters.ModuleID)
- countArgIdx++
- }
- if filters.UserID != nil {
- query += fmt.Sprintf(" AND ta.user_id = $%d", argIdx)
- args = append(args, *filters.UserID)
- argIdx++
- countQuery += fmt.Sprintf(" AND user_id = $%d", countArgIdx)
- countArgs = append(countArgs, *filters.UserID)
- countArgIdx++
- }
- if filters.RoleCode != "" {
- query += fmt.Sprintf(" AND ta.role_code = $%d", argIdx)
- args = append(args, filters.RoleCode)
- argIdx++
- countQuery += fmt.Sprintf(" AND role_code = $%d", countArgIdx)
- countArgs = append(countArgs, filters.RoleCode)
- countArgIdx++
- }
- if filters.Status != "" {
- query += fmt.Sprintf(" AND ta.status = $%d", argIdx)
- args = append(args, string(filters.Status))
- argIdx++
- countQuery += fmt.Sprintf(" AND status = $%d", countArgIdx)
- countArgs = append(countArgs, string(filters.Status))
- countArgIdx++
- }
- if filters.Overdue != nil && *filters.Overdue {
- query += " AND ta.deadline < NOW() AND ta.status IN ('pending', 'in_progress')"
- countQuery += " AND deadline < NOW() AND status IN ('pending', 'in_progress')"
- }
- }
-
- var total int
- err := s.pool.QueryRow(ctx, countQuery, countArgs...).Scan(&total)
- if err != nil {
- return nil, 0, err
- }
-
- query += " ORDER BY ta.deadline ASC"
-
- if filters != nil && filters.Limit > 0 {
- query += fmt.Sprintf(" LIMIT $%d", argIdx)
- args = append(args, filters.Limit)
- argIdx++
- if filters.Offset > 0 {
- query += fmt.Sprintf(" OFFSET $%d", argIdx)
- args = append(args, filters.Offset)
- argIdx++
- }
- }
-
- rows, err := s.pool.Query(ctx, query, args...)
- if err != nil {
- return nil, 0, err
- }
- defer rows.Close()
-
- var assignments []TrainingAssignment
- for rows.Next() {
- var a TrainingAssignment
- var status, triggerType string
-
- err := rows.Scan(
- &a.ID, &a.TenantID, &a.ModuleID, &a.UserID, &a.UserName, &a.UserEmail,
- &a.RoleCode, &triggerType, &a.TriggerEvent, &status, &a.ProgressPercent,
- &a.QuizScore, &a.QuizPassed, &a.QuizAttempts,
- &a.StartedAt, &a.CompletedAt, &a.Deadline, &a.CertificateID,
- &a.EscalationLevel, &a.LastEscalationAt, &a.EnrollmentID,
- &a.CreatedAt, &a.UpdatedAt,
- &a.ModuleCode, &a.ModuleTitle,
- )
- if err != nil {
- return nil, 0, err
- }
-
- a.Status = AssignmentStatus(status)
- a.TriggerType = TriggerType(triggerType)
- assignments = append(assignments, a)
- }
-
- if assignments == nil {
- assignments = []TrainingAssignment{}
- }
-
- return assignments, total, nil
-}
-
-// UpdateAssignmentStatus updates the status and related fields
-func (s *Store) UpdateAssignmentStatus(ctx context.Context, id uuid.UUID, status AssignmentStatus, progress int) error {
- now := time.Now().UTC()
-
- _, err := s.pool.Exec(ctx, `
- UPDATE training_assignments SET
- status = $2,
- progress_percent = $3,
- started_at = CASE
- WHEN started_at IS NULL AND $2 IN ('in_progress', 'completed') THEN $4
- ELSE started_at
- END,
- completed_at = CASE
- WHEN $2 = 'completed' THEN $4
- ELSE completed_at
- END,
- updated_at = $4
- WHERE id = $1
- `, id, string(status), progress, now)
-
- return err
-}
-
-// UpdateAssignmentDeadline updates the deadline of an assignment
-func (s *Store) UpdateAssignmentDeadline(ctx context.Context, id uuid.UUID, deadline time.Time) error {
- now := time.Now().UTC()
- _, err := s.pool.Exec(ctx, `
- UPDATE training_assignments SET
- deadline = $2,
- updated_at = $3
- WHERE id = $1
- `, id, deadline, now)
- return err
-}
-
-// UpdateAssignmentQuizResult updates quiz-related fields on an assignment
-func (s *Store) UpdateAssignmentQuizResult(ctx context.Context, id uuid.UUID, score float64, passed bool, attempts int) error {
- now := time.Now().UTC()
-
- _, err := s.pool.Exec(ctx, `
- UPDATE training_assignments SET
- quiz_score = $2,
- quiz_passed = $3,
- quiz_attempts = $4,
- status = CASE WHEN $3 = true THEN 'completed' ELSE status END,
- completed_at = CASE WHEN $3 = true THEN $5 ELSE completed_at END,
- progress_percent = CASE WHEN $3 = true THEN 100 ELSE progress_percent END,
- updated_at = $5
- WHERE id = $1
- `, id, score, passed, attempts, now)
-
- return err
-}
-
-// ListOverdueAssignments returns assignments past their deadline
-func (s *Store) ListOverdueAssignments(ctx context.Context, tenantID uuid.UUID) ([]TrainingAssignment, error) {
- overdue := true
- assignments, _, err := s.ListAssignments(ctx, tenantID, &AssignmentFilters{
- Overdue: &overdue,
- Limit: 1000,
- })
- return assignments, err
-}
-
-// ============================================================================
-// Quiz Operations
-// ============================================================================
-
-// CreateQuizQuestion creates a new quiz question
-func (s *Store) CreateQuizQuestion(ctx context.Context, q *QuizQuestion) error {
- q.ID = uuid.New()
- q.CreatedAt = time.Now().UTC()
- if !q.IsActive {
- q.IsActive = true
- }
-
- options, _ := json.Marshal(q.Options)
-
- _, err := s.pool.Exec(ctx, `
- INSERT INTO training_quiz_questions (
- id, module_id, question, options, correct_index,
- explanation, difficulty, is_active, sort_order, created_at
- ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
- `,
- q.ID, q.ModuleID, q.Question, options, q.CorrectIndex,
- q.Explanation, string(q.Difficulty), q.IsActive, q.SortOrder, q.CreatedAt,
- )
-
- return err
-}
-
-// ListQuizQuestions lists quiz questions for a module
-func (s *Store) ListQuizQuestions(ctx context.Context, moduleID uuid.UUID) ([]QuizQuestion, error) {
- rows, err := s.pool.Query(ctx, `
- SELECT
- id, module_id, question, options, correct_index,
- explanation, difficulty, is_active, sort_order, created_at
- FROM training_quiz_questions
- WHERE module_id = $1 AND is_active = true
- ORDER BY sort_order ASC, created_at ASC
- `, moduleID)
- if err != nil {
- return nil, err
- }
- defer rows.Close()
-
- var questions []QuizQuestion
- for rows.Next() {
- var q QuizQuestion
- var options []byte
- var difficulty string
-
- err := rows.Scan(
- &q.ID, &q.ModuleID, &q.Question, &options, &q.CorrectIndex,
- &q.Explanation, &difficulty, &q.IsActive, &q.SortOrder, &q.CreatedAt,
- )
- if err != nil {
- return nil, err
- }
-
- q.Difficulty = Difficulty(difficulty)
- json.Unmarshal(options, &q.Options)
- if q.Options == nil {
- q.Options = []string{}
- }
-
- questions = append(questions, q)
- }
-
- if questions == nil {
- questions = []QuizQuestion{}
- }
-
- return questions, nil
-}
-
-// CreateQuizAttempt records a quiz attempt
-func (s *Store) CreateQuizAttempt(ctx context.Context, attempt *QuizAttempt) error {
- attempt.ID = uuid.New()
- attempt.AttemptedAt = time.Now().UTC()
-
- answers, _ := json.Marshal(attempt.Answers)
-
- _, err := s.pool.Exec(ctx, `
- INSERT INTO training_quiz_attempts (
- id, assignment_id, user_id, answers, score,
- passed, correct_count, total_count, duration_seconds, attempted_at
- ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
- `,
- attempt.ID, attempt.AssignmentID, attempt.UserID, answers, attempt.Score,
- attempt.Passed, attempt.CorrectCount, attempt.TotalCount, attempt.DurationSeconds, attempt.AttemptedAt,
- )
-
- return err
-}
-
-// ListQuizAttempts lists quiz attempts for an assignment
-func (s *Store) ListQuizAttempts(ctx context.Context, assignmentID uuid.UUID) ([]QuizAttempt, error) {
- rows, err := s.pool.Query(ctx, `
- SELECT
- id, assignment_id, user_id, answers, score,
- passed, correct_count, total_count, duration_seconds, attempted_at
- FROM training_quiz_attempts
- WHERE assignment_id = $1
- ORDER BY attempted_at DESC
- `, assignmentID)
- if err != nil {
- return nil, err
- }
- defer rows.Close()
-
- var attempts []QuizAttempt
- for rows.Next() {
- var a QuizAttempt
- var answers []byte
-
- err := rows.Scan(
- &a.ID, &a.AssignmentID, &a.UserID, &answers, &a.Score,
- &a.Passed, &a.CorrectCount, &a.TotalCount, &a.DurationSeconds, &a.AttemptedAt,
- )
- if err != nil {
- return nil, err
- }
-
- json.Unmarshal(answers, &a.Answers)
- if a.Answers == nil {
- a.Answers = []QuizAnswer{}
- }
-
- attempts = append(attempts, a)
- }
-
- if attempts == nil {
- attempts = []QuizAttempt{}
- }
-
- return attempts, nil
-}
-
-// ============================================================================
-// Content Operations
-// ============================================================================
-
-// CreateModuleContent creates new content for a module
-func (s *Store) CreateModuleContent(ctx context.Context, content *ModuleContent) error {
- content.ID = uuid.New()
- content.CreatedAt = time.Now().UTC()
- content.UpdatedAt = content.CreatedAt
-
- // Auto-increment version
- var maxVersion int
- s.pool.QueryRow(ctx,
- "SELECT COALESCE(MAX(version), 0) FROM training_module_content WHERE module_id = $1",
- content.ModuleID).Scan(&maxVersion)
- content.Version = maxVersion + 1
-
- _, err := s.pool.Exec(ctx, `
- INSERT INTO training_module_content (
- id, module_id, version, content_format, content_body,
- summary, generated_by, llm_model, is_published,
- reviewed_by, reviewed_at, created_at, updated_at
- ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)
- `,
- content.ID, content.ModuleID, content.Version, string(content.ContentFormat), content.ContentBody,
- content.Summary, content.GeneratedBy, content.LLMModel, content.IsPublished,
- content.ReviewedBy, content.ReviewedAt, content.CreatedAt, content.UpdatedAt,
- )
-
- return err
-}
-
-// GetPublishedContent retrieves the published content for a module
-func (s *Store) GetPublishedContent(ctx context.Context, moduleID uuid.UUID) (*ModuleContent, error) {
- var content ModuleContent
- var contentFormat string
-
- err := s.pool.QueryRow(ctx, `
- SELECT
- id, module_id, version, content_format, content_body,
- summary, generated_by, llm_model, is_published,
- reviewed_by, reviewed_at, created_at, updated_at
- FROM training_module_content
- WHERE module_id = $1 AND is_published = true
- ORDER BY version DESC
- LIMIT 1
- `, moduleID).Scan(
- &content.ID, &content.ModuleID, &content.Version, &contentFormat, &content.ContentBody,
- &content.Summary, &content.GeneratedBy, &content.LLMModel, &content.IsPublished,
- &content.ReviewedBy, &content.ReviewedAt, &content.CreatedAt, &content.UpdatedAt,
- )
-
- if err == pgx.ErrNoRows {
- return nil, nil
- }
- if err != nil {
- return nil, err
- }
-
- content.ContentFormat = ContentFormat(contentFormat)
- return &content, nil
-}
-
-// GetLatestContent retrieves the latest content (published or not) for a module
-func (s *Store) GetLatestContent(ctx context.Context, moduleID uuid.UUID) (*ModuleContent, error) {
- var content ModuleContent
- var contentFormat string
-
- err := s.pool.QueryRow(ctx, `
- SELECT
- id, module_id, version, content_format, content_body,
- summary, generated_by, llm_model, is_published,
- reviewed_by, reviewed_at, created_at, updated_at
- FROM training_module_content
- WHERE module_id = $1
- ORDER BY version DESC
- LIMIT 1
- `, moduleID).Scan(
- &content.ID, &content.ModuleID, &content.Version, &contentFormat, &content.ContentBody,
- &content.Summary, &content.GeneratedBy, &content.LLMModel, &content.IsPublished,
- &content.ReviewedBy, &content.ReviewedAt, &content.CreatedAt, &content.UpdatedAt,
- )
-
- if err == pgx.ErrNoRows {
- return nil, nil
- }
- if err != nil {
- return nil, err
- }
-
- content.ContentFormat = ContentFormat(contentFormat)
- return &content, nil
-}
-
-// PublishContent marks a content version as published (unpublishes all others for that module)
-func (s *Store) PublishContent(ctx context.Context, contentID uuid.UUID, reviewedBy uuid.UUID) error {
- now := time.Now().UTC()
-
- // Get module_id for this content
- var moduleID uuid.UUID
- err := s.pool.QueryRow(ctx,
- "SELECT module_id FROM training_module_content WHERE id = $1",
- contentID).Scan(&moduleID)
- if err != nil {
- return err
- }
-
- // Unpublish all existing content for this module
- _, err = s.pool.Exec(ctx,
- "UPDATE training_module_content SET is_published = false WHERE module_id = $1",
- moduleID)
- if err != nil {
- return err
- }
-
- // Publish the specified content
- _, err = s.pool.Exec(ctx, `
- UPDATE training_module_content SET
- is_published = true, reviewed_by = $2, reviewed_at = $3, updated_at = $3
- WHERE id = $1
- `, contentID, reviewedBy, now)
-
- return err
-}
-
-// ============================================================================
-// Audit Log Operations
-// ============================================================================
-
-// LogAction creates an audit log entry
-func (s *Store) LogAction(ctx context.Context, entry *AuditLogEntry) error {
- entry.ID = uuid.New()
- entry.CreatedAt = time.Now().UTC()
-
- details, _ := json.Marshal(entry.Details)
-
- _, err := s.pool.Exec(ctx, `
- INSERT INTO training_audit_log (
- id, tenant_id, user_id, action, entity_type,
- entity_id, details, ip_address, created_at
- ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
- `,
- entry.ID, entry.TenantID, entry.UserID, string(entry.Action), string(entry.EntityType),
- entry.EntityID, details, entry.IPAddress, entry.CreatedAt,
- )
-
- return err
-}
-
-// ListAuditLog lists audit log entries for a tenant
-func (s *Store) ListAuditLog(ctx context.Context, tenantID uuid.UUID, filters *AuditLogFilters) ([]AuditLogEntry, int, error) {
- countQuery := "SELECT COUNT(*) FROM training_audit_log WHERE tenant_id = $1"
- countArgs := []interface{}{tenantID}
- countArgIdx := 2
-
- query := `
- SELECT
- id, tenant_id, user_id, action, entity_type,
- entity_id, details, ip_address, created_at
- FROM training_audit_log WHERE tenant_id = $1`
-
- args := []interface{}{tenantID}
- argIdx := 2
-
- if filters != nil {
- if filters.UserID != nil {
- query += fmt.Sprintf(" AND user_id = $%d", argIdx)
- args = append(args, *filters.UserID)
- argIdx++
- countQuery += fmt.Sprintf(" AND user_id = $%d", countArgIdx)
- countArgs = append(countArgs, *filters.UserID)
- countArgIdx++
- }
- if filters.Action != "" {
- query += fmt.Sprintf(" AND action = $%d", argIdx)
- args = append(args, string(filters.Action))
- argIdx++
- countQuery += fmt.Sprintf(" AND action = $%d", countArgIdx)
- countArgs = append(countArgs, string(filters.Action))
- countArgIdx++
- }
- if filters.EntityType != "" {
- query += fmt.Sprintf(" AND entity_type = $%d", argIdx)
- args = append(args, string(filters.EntityType))
- argIdx++
- countQuery += fmt.Sprintf(" AND entity_type = $%d", countArgIdx)
- countArgs = append(countArgs, string(filters.EntityType))
- countArgIdx++
- }
- }
-
- var total int
- err := s.pool.QueryRow(ctx, countQuery, countArgs...).Scan(&total)
- if err != nil {
- return nil, 0, err
- }
-
- query += " ORDER BY created_at DESC"
-
- if filters != nil && filters.Limit > 0 {
- query += fmt.Sprintf(" LIMIT $%d", argIdx)
- args = append(args, filters.Limit)
- argIdx++
- if filters.Offset > 0 {
- query += fmt.Sprintf(" OFFSET $%d", argIdx)
- args = append(args, filters.Offset)
- argIdx++
- }
- }
-
- rows, err := s.pool.Query(ctx, query, args...)
- if err != nil {
- return nil, 0, err
- }
- defer rows.Close()
-
- var entries []AuditLogEntry
- for rows.Next() {
- var entry AuditLogEntry
- var action, entityType string
- var details []byte
-
- err := rows.Scan(
- &entry.ID, &entry.TenantID, &entry.UserID, &action, &entityType,
- &entry.EntityID, &details, &entry.IPAddress, &entry.CreatedAt,
- )
- if err != nil {
- return nil, 0, err
- }
-
- entry.Action = AuditAction(action)
- entry.EntityType = AuditEntityType(entityType)
- json.Unmarshal(details, &entry.Details)
- if entry.Details == nil {
- entry.Details = map[string]interface{}{}
- }
-
- entries = append(entries, entry)
- }
-
- if entries == nil {
- entries = []AuditLogEntry{}
- }
-
- return entries, total, nil
-}
-
-// ============================================================================
-// Statistics
-// ============================================================================
-
-// GetTrainingStats returns aggregated training statistics for a tenant
-func (s *Store) GetTrainingStats(ctx context.Context, tenantID uuid.UUID) (*TrainingStats, error) {
- stats := &TrainingStats{}
-
- // Total active modules
- s.pool.QueryRow(ctx,
- "SELECT COUNT(*) FROM training_modules WHERE tenant_id = $1 AND is_active = true",
- tenantID).Scan(&stats.TotalModules)
-
- // Total assignments
- s.pool.QueryRow(ctx,
- "SELECT COUNT(*) FROM training_assignments WHERE tenant_id = $1",
- tenantID).Scan(&stats.TotalAssignments)
-
- // Status counts
- s.pool.QueryRow(ctx,
- "SELECT COUNT(*) FROM training_assignments WHERE tenant_id = $1 AND status = 'pending'",
- tenantID).Scan(&stats.PendingCount)
-
- s.pool.QueryRow(ctx,
- "SELECT COUNT(*) FROM training_assignments WHERE tenant_id = $1 AND status = 'in_progress'",
- tenantID).Scan(&stats.InProgressCount)
-
- s.pool.QueryRow(ctx,
- "SELECT COUNT(*) FROM training_assignments WHERE tenant_id = $1 AND status = 'completed'",
- tenantID).Scan(&stats.CompletedCount)
-
- // Completion rate
- if stats.TotalAssignments > 0 {
- stats.CompletionRate = float64(stats.CompletedCount) / float64(stats.TotalAssignments) * 100
- }
-
- // Overdue count
- s.pool.QueryRow(ctx, `
- SELECT COUNT(*) FROM training_assignments
- WHERE tenant_id = $1
- AND status IN ('pending', 'in_progress')
- AND deadline < NOW()
- `, tenantID).Scan(&stats.OverdueCount)
-
- // Average quiz score
- s.pool.QueryRow(ctx, `
- SELECT COALESCE(AVG(quiz_score), 0) FROM training_assignments
- WHERE tenant_id = $1 AND quiz_score IS NOT NULL
- `, tenantID).Scan(&stats.AvgQuizScore)
-
- // Average completion days
- s.pool.QueryRow(ctx, `
- SELECT COALESCE(AVG(EXTRACT(EPOCH FROM (completed_at - started_at)) / 86400), 0)
- FROM training_assignments
- WHERE tenant_id = $1 AND status = 'completed'
- AND started_at IS NOT NULL AND completed_at IS NOT NULL
- `, tenantID).Scan(&stats.AvgCompletionDays)
-
- // Upcoming deadlines (within 7 days)
- s.pool.QueryRow(ctx, `
- SELECT COUNT(*) FROM training_assignments
- WHERE tenant_id = $1
- AND status IN ('pending', 'in_progress')
- AND deadline BETWEEN NOW() AND NOW() + INTERVAL '7 days'
- `, tenantID).Scan(&stats.UpcomingDeadlines)
-
- return stats, nil
-}
-
-// GetDeadlines returns upcoming deadlines for a tenant
-func (s *Store) GetDeadlines(ctx context.Context, tenantID uuid.UUID, limit int) ([]DeadlineInfo, error) {
- if limit <= 0 {
- limit = 20
- }
-
- rows, err := s.pool.Query(ctx, `
- SELECT
- ta.id, m.module_code, m.title,
- ta.user_id, ta.user_name, ta.deadline, ta.status,
- EXTRACT(DAY FROM (ta.deadline - NOW()))::INT AS days_left
- FROM training_assignments ta
- JOIN training_modules m ON m.id = ta.module_id
- WHERE ta.tenant_id = $1
- AND ta.status IN ('pending', 'in_progress')
- ORDER BY ta.deadline ASC
- LIMIT $2
- `, tenantID, limit)
- if err != nil {
- return nil, err
- }
- defer rows.Close()
-
- var deadlines []DeadlineInfo
- for rows.Next() {
- var d DeadlineInfo
- var status string
-
- err := rows.Scan(
- &d.AssignmentID, &d.ModuleCode, &d.ModuleTitle,
- &d.UserID, &d.UserName, &d.Deadline, &status,
- &d.DaysLeft,
- )
- if err != nil {
- return nil, err
- }
-
- d.Status = AssignmentStatus(status)
- deadlines = append(deadlines, d)
- }
-
- if deadlines == nil {
- deadlines = []DeadlineInfo{}
- }
-
- return deadlines, nil
-}
-
-// ============================================================================
-// Media CRUD Operations
-// ============================================================================
-
-// CreateMedia creates a new media record
-func (s *Store) CreateMedia(ctx context.Context, media *TrainingMedia) error {
- media.ID = uuid.New()
- media.CreatedAt = time.Now().UTC()
- media.UpdatedAt = media.CreatedAt
- if media.Metadata == nil {
- media.Metadata = json.RawMessage("{}")
- }
-
- _, err := s.pool.Exec(ctx, `
- INSERT INTO training_media (
- id, module_id, content_id, media_type, status,
- bucket, object_key, file_size_bytes, duration_seconds,
- mime_type, voice_model, language, metadata,
- error_message, generated_by, is_published, created_at, updated_at
- ) VALUES (
- $1, $2, $3, $4, $5,
- $6, $7, $8, $9,
- $10, $11, $12, $13,
- $14, $15, $16, $17, $18
- )
- `,
- media.ID, media.ModuleID, media.ContentID, string(media.MediaType), string(media.Status),
- media.Bucket, media.ObjectKey, media.FileSizeBytes, media.DurationSeconds,
- media.MimeType, media.VoiceModel, media.Language, media.Metadata,
- media.ErrorMessage, media.GeneratedBy, media.IsPublished, media.CreatedAt, media.UpdatedAt,
- )
-
- return err
-}
-
-// GetMedia retrieves a media record by ID
-func (s *Store) GetMedia(ctx context.Context, id uuid.UUID) (*TrainingMedia, error) {
- var media TrainingMedia
- var mediaType, status string
-
- err := s.pool.QueryRow(ctx, `
- SELECT id, module_id, content_id, media_type, status,
- bucket, object_key, file_size_bytes, duration_seconds,
- mime_type, voice_model, language, metadata,
- error_message, generated_by, is_published, created_at, updated_at
- FROM training_media WHERE id = $1
- `, id).Scan(
- &media.ID, &media.ModuleID, &media.ContentID, &mediaType, &status,
- &media.Bucket, &media.ObjectKey, &media.FileSizeBytes, &media.DurationSeconds,
- &media.MimeType, &media.VoiceModel, &media.Language, &media.Metadata,
- &media.ErrorMessage, &media.GeneratedBy, &media.IsPublished, &media.CreatedAt, &media.UpdatedAt,
- )
-
- if err == pgx.ErrNoRows {
- return nil, nil
- }
- if err != nil {
- return nil, err
- }
-
- media.MediaType = MediaType(mediaType)
- media.Status = MediaStatus(status)
- return &media, nil
-}
-
-// GetMediaForModule retrieves all media for a module
-func (s *Store) GetMediaForModule(ctx context.Context, moduleID uuid.UUID) ([]TrainingMedia, error) {
- rows, err := s.pool.Query(ctx, `
- SELECT id, module_id, content_id, media_type, status,
- bucket, object_key, file_size_bytes, duration_seconds,
- mime_type, voice_model, language, metadata,
- error_message, generated_by, is_published, created_at, updated_at
- FROM training_media WHERE module_id = $1
- ORDER BY media_type, created_at DESC
- `, moduleID)
- if err != nil {
- return nil, err
- }
- defer rows.Close()
-
- var mediaList []TrainingMedia
- for rows.Next() {
- var media TrainingMedia
- var mediaType, status string
- if err := rows.Scan(
- &media.ID, &media.ModuleID, &media.ContentID, &mediaType, &status,
- &media.Bucket, &media.ObjectKey, &media.FileSizeBytes, &media.DurationSeconds,
- &media.MimeType, &media.VoiceModel, &media.Language, &media.Metadata,
- &media.ErrorMessage, &media.GeneratedBy, &media.IsPublished, &media.CreatedAt, &media.UpdatedAt,
- ); err != nil {
- return nil, err
- }
- media.MediaType = MediaType(mediaType)
- media.Status = MediaStatus(status)
- mediaList = append(mediaList, media)
- }
-
- if mediaList == nil {
- mediaList = []TrainingMedia{}
- }
- return mediaList, nil
-}
-
-// UpdateMediaStatus updates the status and related fields of a media record
-func (s *Store) UpdateMediaStatus(ctx context.Context, id uuid.UUID, status MediaStatus, sizeBytes int64, duration float64, errMsg string) error {
- _, err := s.pool.Exec(ctx, `
- UPDATE training_media
- SET status = $2, file_size_bytes = $3, duration_seconds = $4,
- error_message = $5, updated_at = NOW()
- WHERE id = $1
- `, id, string(status), sizeBytes, duration, errMsg)
- return err
-}
-
-// PublishMedia publishes or unpublishes a media record
-func (s *Store) PublishMedia(ctx context.Context, id uuid.UUID, publish bool) error {
- _, err := s.pool.Exec(ctx, `
- UPDATE training_media SET is_published = $2, updated_at = NOW() WHERE id = $1
- `, id, publish)
- return err
-}
-
-// GetPublishedAudio gets the published audio for a module
-func (s *Store) GetPublishedAudio(ctx context.Context, moduleID uuid.UUID) (*TrainingMedia, error) {
- var media TrainingMedia
- var mediaType, status string
-
- err := s.pool.QueryRow(ctx, `
- SELECT id, module_id, content_id, media_type, status,
- bucket, object_key, file_size_bytes, duration_seconds,
- mime_type, voice_model, language, metadata,
- error_message, generated_by, is_published, created_at, updated_at
- FROM training_media
- WHERE module_id = $1 AND media_type = 'audio' AND is_published = true
- ORDER BY created_at DESC LIMIT 1
- `, moduleID).Scan(
- &media.ID, &media.ModuleID, &media.ContentID, &mediaType, &status,
- &media.Bucket, &media.ObjectKey, &media.FileSizeBytes, &media.DurationSeconds,
- &media.MimeType, &media.VoiceModel, &media.Language, &media.Metadata,
- &media.ErrorMessage, &media.GeneratedBy, &media.IsPublished, &media.CreatedAt, &media.UpdatedAt,
- )
-
- if err == pgx.ErrNoRows {
- return nil, nil
- }
- if err != nil {
- return nil, err
- }
-
- media.MediaType = MediaType(mediaType)
- media.Status = MediaStatus(status)
- return &media, nil
-}
-
-// SetCertificateID sets the certificate ID on an assignment
-func (s *Store) SetCertificateID(ctx context.Context, assignmentID, certID uuid.UUID) error {
- _, err := s.pool.Exec(ctx, `
- UPDATE training_assignments SET certificate_id = $2, updated_at = NOW() WHERE id = $1
- `, assignmentID, certID)
- return err
-}
-
-// GetAssignmentByCertificateID finds an assignment by its certificate ID
-func (s *Store) GetAssignmentByCertificateID(ctx context.Context, certID uuid.UUID) (*TrainingAssignment, error) {
- var assignmentID uuid.UUID
- err := s.pool.QueryRow(ctx,
- "SELECT id FROM training_assignments WHERE certificate_id = $1",
- certID).Scan(&assignmentID)
- if err == pgx.ErrNoRows {
- return nil, nil
- }
- if err != nil {
- return nil, err
- }
- return s.GetAssignment(ctx, assignmentID)
-}
-
-// ListCertificates lists assignments that have certificates for a tenant
-func (s *Store) ListCertificates(ctx context.Context, tenantID uuid.UUID) ([]TrainingAssignment, error) {
- rows, err := s.pool.Query(ctx, `
- SELECT
- ta.id, ta.tenant_id, ta.module_id, ta.user_id, ta.user_name, ta.user_email,
- ta.role_code, ta.trigger_type, ta.trigger_event, ta.status, ta.progress_percent,
- ta.quiz_score, ta.quiz_passed, ta.quiz_attempts,
- ta.started_at, ta.completed_at, ta.deadline, ta.certificate_id,
- ta.escalation_level, ta.last_escalation_at, ta.enrollment_id,
- ta.created_at, ta.updated_at,
- m.module_code, m.title
- FROM training_assignments ta
- JOIN training_modules m ON m.id = ta.module_id
- WHERE ta.tenant_id = $1 AND ta.certificate_id IS NOT NULL
- ORDER BY ta.completed_at DESC
- `, tenantID)
- if err != nil {
- return nil, err
- }
- defer rows.Close()
-
- var assignments []TrainingAssignment
- for rows.Next() {
- var a TrainingAssignment
- var status, triggerType string
-
- err := rows.Scan(
- &a.ID, &a.TenantID, &a.ModuleID, &a.UserID, &a.UserName, &a.UserEmail,
- &a.RoleCode, &triggerType, &a.TriggerEvent, &status, &a.ProgressPercent,
- &a.QuizScore, &a.QuizPassed, &a.QuizAttempts,
- &a.StartedAt, &a.CompletedAt, &a.Deadline, &a.CertificateID,
- &a.EscalationLevel, &a.LastEscalationAt, &a.EnrollmentID,
- &a.CreatedAt, &a.UpdatedAt,
- &a.ModuleCode, &a.ModuleTitle,
- )
- if err != nil {
- return nil, err
- }
-
- a.Status = AssignmentStatus(status)
- a.TriggerType = TriggerType(triggerType)
- assignments = append(assignments, a)
- }
-
- if assignments == nil {
- assignments = []TrainingAssignment{}
- }
-
- return assignments, nil
-}
-
-// GetPublishedVideo gets the published video for a module
-func (s *Store) GetPublishedVideo(ctx context.Context, moduleID uuid.UUID) (*TrainingMedia, error) {
- var media TrainingMedia
- var mediaType, status string
-
- err := s.pool.QueryRow(ctx, `
- SELECT id, module_id, content_id, media_type, status,
- bucket, object_key, file_size_bytes, duration_seconds,
- mime_type, voice_model, language, metadata,
- error_message, generated_by, is_published, created_at, updated_at
- FROM training_media
- WHERE module_id = $1 AND media_type = 'video' AND is_published = true
- ORDER BY created_at DESC LIMIT 1
- `, moduleID).Scan(
- &media.ID, &media.ModuleID, &media.ContentID, &mediaType, &status,
- &media.Bucket, &media.ObjectKey, &media.FileSizeBytes, &media.DurationSeconds,
- &media.MimeType, &media.VoiceModel, &media.Language, &media.Metadata,
- &media.ErrorMessage, &media.GeneratedBy, &media.IsPublished, &media.CreatedAt, &media.UpdatedAt,
- )
-
- if err == pgx.ErrNoRows {
- return nil, nil
- }
- if err != nil {
- return nil, err
- }
-
- media.MediaType = MediaType(mediaType)
- media.Status = MediaStatus(status)
- return &media, nil
-}
-
-// ============================================================================
-// Checkpoint Operations
-// ============================================================================
-
-// CreateCheckpoint inserts a new checkpoint
-func (s *Store) CreateCheckpoint(ctx context.Context, cp *Checkpoint) error {
- cp.ID = uuid.New()
- cp.CreatedAt = time.Now().UTC()
-
- _, err := s.pool.Exec(ctx, `
- INSERT INTO training_checkpoints (id, module_id, checkpoint_index, title, timestamp_seconds, created_at)
- VALUES ($1, $2, $3, $4, $5, $6)
- `, cp.ID, cp.ModuleID, cp.CheckpointIndex, cp.Title, cp.TimestampSeconds, cp.CreatedAt)
-
- return err
-}
-
-// ListCheckpoints returns all checkpoints for a module ordered by index
-func (s *Store) ListCheckpoints(ctx context.Context, moduleID uuid.UUID) ([]Checkpoint, error) {
- rows, err := s.pool.Query(ctx, `
- SELECT id, module_id, checkpoint_index, title, timestamp_seconds, created_at
- FROM training_checkpoints
- WHERE module_id = $1
- ORDER BY checkpoint_index
- `, moduleID)
- if err != nil {
- return nil, err
- }
- defer rows.Close()
-
- var checkpoints []Checkpoint
- for rows.Next() {
- var cp Checkpoint
- if err := rows.Scan(&cp.ID, &cp.ModuleID, &cp.CheckpointIndex, &cp.Title, &cp.TimestampSeconds, &cp.CreatedAt); err != nil {
- return nil, err
- }
- checkpoints = append(checkpoints, cp)
- }
-
- if checkpoints == nil {
- checkpoints = []Checkpoint{}
- }
- return checkpoints, nil
-}
-
-// DeleteCheckpointsForModule removes all checkpoints for a module (used before regenerating)
-func (s *Store) DeleteCheckpointsForModule(ctx context.Context, moduleID uuid.UUID) error {
- _, err := s.pool.Exec(ctx, `DELETE FROM training_checkpoints WHERE module_id = $1`, moduleID)
- return err
-}
-
-// GetCheckpointProgress retrieves progress for a specific checkpoint+assignment
-func (s *Store) GetCheckpointProgress(ctx context.Context, assignmentID, checkpointID uuid.UUID) (*CheckpointProgress, error) {
- var cp CheckpointProgress
- err := s.pool.QueryRow(ctx, `
- SELECT id, assignment_id, checkpoint_id, passed, attempts, last_attempt_at, created_at
- FROM training_checkpoint_progress
- WHERE assignment_id = $1 AND checkpoint_id = $2
- `, assignmentID, checkpointID).Scan(
- &cp.ID, &cp.AssignmentID, &cp.CheckpointID, &cp.Passed, &cp.Attempts, &cp.LastAttemptAt, &cp.CreatedAt,
- )
- if err == pgx.ErrNoRows {
- return nil, nil
- }
- if err != nil {
- return nil, err
- }
- return &cp, nil
-}
-
-// UpsertCheckpointProgress creates or updates checkpoint progress
-func (s *Store) UpsertCheckpointProgress(ctx context.Context, progress *CheckpointProgress) error {
- progress.ID = uuid.New()
- now := time.Now().UTC()
- progress.LastAttemptAt = &now
- progress.CreatedAt = now
-
- _, err := s.pool.Exec(ctx, `
- INSERT INTO training_checkpoint_progress (id, assignment_id, checkpoint_id, passed, attempts, last_attempt_at, created_at)
- VALUES ($1, $2, $3, $4, $5, $6, $7)
- ON CONFLICT (assignment_id, checkpoint_id) DO UPDATE SET
- passed = EXCLUDED.passed,
- attempts = training_checkpoint_progress.attempts + 1,
- last_attempt_at = EXCLUDED.last_attempt_at
- `, progress.ID, progress.AssignmentID, progress.CheckpointID, progress.Passed, progress.Attempts, progress.LastAttemptAt, progress.CreatedAt)
-
- return err
-}
-
-// GetCheckpointQuestions retrieves quiz questions for a specific checkpoint
-func (s *Store) GetCheckpointQuestions(ctx context.Context, checkpointID uuid.UUID) ([]QuizQuestion, error) {
- rows, err := s.pool.Query(ctx, `
- SELECT id, module_id, question, options, correct_index, explanation, difficulty, is_active, sort_order, created_at
- FROM training_quiz_questions
- WHERE checkpoint_id = $1 AND is_active = true
- ORDER BY sort_order
- `, checkpointID)
- if err != nil {
- return nil, err
- }
- defer rows.Close()
-
- var questions []QuizQuestion
- for rows.Next() {
- var q QuizQuestion
- var options []byte
- var difficulty string
- if err := rows.Scan(&q.ID, &q.ModuleID, &q.Question, &options, &q.CorrectIndex, &q.Explanation, &difficulty, &q.IsActive, &q.SortOrder, &q.CreatedAt); err != nil {
- return nil, err
- }
- json.Unmarshal(options, &q.Options)
- q.Difficulty = Difficulty(difficulty)
- questions = append(questions, q)
- }
-
- if questions == nil {
- questions = []QuizQuestion{}
- }
- return questions, nil
-}
-
-// CreateCheckpointQuizQuestion creates a quiz question linked to a checkpoint
-func (s *Store) CreateCheckpointQuizQuestion(ctx context.Context, q *QuizQuestion, checkpointID uuid.UUID) error {
- q.ID = uuid.New()
- q.CreatedAt = time.Now().UTC()
- q.IsActive = true
-
- options, _ := json.Marshal(q.Options)
-
- _, err := s.pool.Exec(ctx, `
- INSERT INTO training_quiz_questions (id, module_id, checkpoint_id, question, options, correct_index, explanation, difficulty, is_active, sort_order, created_at)
- VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
- `, q.ID, q.ModuleID, checkpointID, q.Question, options, q.CorrectIndex, q.Explanation, string(q.Difficulty), q.IsActive, q.SortOrder, q.CreatedAt)
-
- return err
-}
-
-// AreAllCheckpointsPassed checks if all checkpoints for a module are passed by an assignment
-func (s *Store) AreAllCheckpointsPassed(ctx context.Context, assignmentID, moduleID uuid.UUID) (bool, error) {
- var totalCheckpoints, passedCheckpoints int
-
- err := s.pool.QueryRow(ctx, `
- SELECT COUNT(*) FROM training_checkpoints WHERE module_id = $1
- `, moduleID).Scan(&totalCheckpoints)
- if err != nil {
- return false, err
- }
-
- if totalCheckpoints == 0 {
- return true, nil
- }
-
- err = s.pool.QueryRow(ctx, `
- SELECT COUNT(*) FROM training_checkpoint_progress cp
- JOIN training_checkpoints c ON cp.checkpoint_id = c.id
- WHERE cp.assignment_id = $1 AND c.module_id = $2 AND cp.passed = true
- `, assignmentID, moduleID).Scan(&passedCheckpoints)
- if err != nil {
- return false, err
- }
-
- return passedCheckpoints >= totalCheckpoints, nil
-}
-
-// ListCheckpointProgress returns all checkpoint progress for an assignment
-func (s *Store) ListCheckpointProgress(ctx context.Context, assignmentID uuid.UUID) ([]CheckpointProgress, error) {
- rows, err := s.pool.Query(ctx, `
- SELECT id, assignment_id, checkpoint_id, passed, attempts, last_attempt_at, created_at
- FROM training_checkpoint_progress
- WHERE assignment_id = $1
- ORDER BY created_at
- `, assignmentID)
- if err != nil {
- return nil, err
- }
- defer rows.Close()
-
- var progress []CheckpointProgress
- for rows.Next() {
- var cp CheckpointProgress
- if err := rows.Scan(&cp.ID, &cp.AssignmentID, &cp.CheckpointID, &cp.Passed, &cp.Attempts, &cp.LastAttemptAt, &cp.CreatedAt); err != nil {
- return nil, err
- }
- progress = append(progress, cp)
- }
-
- if progress == nil {
- progress = []CheckpointProgress{}
- }
- return progress, nil
-}
diff --git a/ai-compliance-sdk/internal/training/store_assignments.go b/ai-compliance-sdk/internal/training/store_assignments.go
new file mode 100644
index 0000000..5e75dca
--- /dev/null
+++ b/ai-compliance-sdk/internal/training/store_assignments.go
@@ -0,0 +1,340 @@
+package training
+
+import (
+ "context"
+ "fmt"
+ "time"
+
+ "github.com/google/uuid"
+ "github.com/jackc/pgx/v5"
+)
+
+// CreateAssignment creates a new training assignment
+func (s *Store) CreateAssignment(ctx context.Context, assignment *TrainingAssignment) error {
+ assignment.ID = uuid.New()
+ assignment.CreatedAt = time.Now().UTC()
+ assignment.UpdatedAt = assignment.CreatedAt
+ if assignment.Status == "" {
+ assignment.Status = AssignmentStatusPending
+ }
+
+ _, err := s.pool.Exec(ctx, `
+ INSERT INTO training_assignments (
+ id, tenant_id, module_id, user_id, user_name, user_email,
+ role_code, trigger_type, trigger_event, status, progress_percent,
+ quiz_score, quiz_passed, quiz_attempts,
+ started_at, completed_at, deadline, certificate_id,
+ escalation_level, last_escalation_at, enrollment_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
+ )
+ `,
+ assignment.ID, assignment.TenantID, assignment.ModuleID, assignment.UserID, assignment.UserName, assignment.UserEmail,
+ assignment.RoleCode, string(assignment.TriggerType), assignment.TriggerEvent, string(assignment.Status), assignment.ProgressPercent,
+ assignment.QuizScore, assignment.QuizPassed, assignment.QuizAttempts,
+ assignment.StartedAt, assignment.CompletedAt, assignment.Deadline, assignment.CertificateID,
+ assignment.EscalationLevel, assignment.LastEscalationAt, assignment.EnrollmentID,
+ assignment.CreatedAt, assignment.UpdatedAt,
+ )
+
+ return err
+}
+
+// GetAssignment retrieves an assignment by ID
+func (s *Store) GetAssignment(ctx context.Context, id uuid.UUID) (*TrainingAssignment, error) {
+ var a TrainingAssignment
+ var status, triggerType string
+
+ err := s.pool.QueryRow(ctx, `
+ SELECT
+ ta.id, ta.tenant_id, ta.module_id, ta.user_id, ta.user_name, ta.user_email,
+ ta.role_code, ta.trigger_type, ta.trigger_event, ta.status, ta.progress_percent,
+ ta.quiz_score, ta.quiz_passed, ta.quiz_attempts,
+ ta.started_at, ta.completed_at, ta.deadline, ta.certificate_id,
+ ta.escalation_level, ta.last_escalation_at, ta.enrollment_id,
+ ta.created_at, ta.updated_at,
+ m.module_code, m.title
+ FROM training_assignments ta
+ JOIN training_modules m ON m.id = ta.module_id
+ WHERE ta.id = $1
+ `, id).Scan(
+ &a.ID, &a.TenantID, &a.ModuleID, &a.UserID, &a.UserName, &a.UserEmail,
+ &a.RoleCode, &triggerType, &a.TriggerEvent, &status, &a.ProgressPercent,
+ &a.QuizScore, &a.QuizPassed, &a.QuizAttempts,
+ &a.StartedAt, &a.CompletedAt, &a.Deadline, &a.CertificateID,
+ &a.EscalationLevel, &a.LastEscalationAt, &a.EnrollmentID,
+ &a.CreatedAt, &a.UpdatedAt,
+ &a.ModuleCode, &a.ModuleTitle,
+ )
+
+ if err == pgx.ErrNoRows {
+ return nil, nil
+ }
+ if err != nil {
+ return nil, err
+ }
+
+ a.Status = AssignmentStatus(status)
+ a.TriggerType = TriggerType(triggerType)
+ return &a, nil
+}
+
+// ListAssignments lists assignments for a tenant with optional filters
+func (s *Store) ListAssignments(ctx context.Context, tenantID uuid.UUID, filters *AssignmentFilters) ([]TrainingAssignment, int, error) {
+ countQuery := "SELECT COUNT(*) FROM training_assignments WHERE tenant_id = $1"
+ countArgs := []interface{}{tenantID}
+ countArgIdx := 2
+
+ query := `
+ SELECT
+ ta.id, ta.tenant_id, ta.module_id, ta.user_id, ta.user_name, ta.user_email,
+ ta.role_code, ta.trigger_type, ta.trigger_event, ta.status, ta.progress_percent,
+ ta.quiz_score, ta.quiz_passed, ta.quiz_attempts,
+ ta.started_at, ta.completed_at, ta.deadline, ta.certificate_id,
+ ta.escalation_level, ta.last_escalation_at, ta.enrollment_id,
+ ta.created_at, ta.updated_at,
+ m.module_code, m.title
+ FROM training_assignments ta
+ JOIN training_modules m ON m.id = ta.module_id
+ WHERE ta.tenant_id = $1`
+
+ args := []interface{}{tenantID}
+ argIdx := 2
+
+ if filters != nil {
+ if filters.ModuleID != nil {
+ query += fmt.Sprintf(" AND ta.module_id = $%d", argIdx)
+ args = append(args, *filters.ModuleID)
+ argIdx++
+ countQuery += fmt.Sprintf(" AND module_id = $%d", countArgIdx)
+ countArgs = append(countArgs, *filters.ModuleID)
+ countArgIdx++
+ }
+ if filters.UserID != nil {
+ query += fmt.Sprintf(" AND ta.user_id = $%d", argIdx)
+ args = append(args, *filters.UserID)
+ argIdx++
+ countQuery += fmt.Sprintf(" AND user_id = $%d", countArgIdx)
+ countArgs = append(countArgs, *filters.UserID)
+ countArgIdx++
+ }
+ if filters.RoleCode != "" {
+ query += fmt.Sprintf(" AND ta.role_code = $%d", argIdx)
+ args = append(args, filters.RoleCode)
+ argIdx++
+ countQuery += fmt.Sprintf(" AND role_code = $%d", countArgIdx)
+ countArgs = append(countArgs, filters.RoleCode)
+ countArgIdx++
+ }
+ if filters.Status != "" {
+ query += fmt.Sprintf(" AND ta.status = $%d", argIdx)
+ args = append(args, string(filters.Status))
+ argIdx++
+ countQuery += fmt.Sprintf(" AND status = $%d", countArgIdx)
+ countArgs = append(countArgs, string(filters.Status))
+ countArgIdx++
+ }
+ if filters.Overdue != nil && *filters.Overdue {
+ query += " AND ta.deadline < NOW() AND ta.status IN ('pending', 'in_progress')"
+ countQuery += " AND deadline < NOW() AND status IN ('pending', 'in_progress')"
+ }
+ }
+
+ var total int
+ err := s.pool.QueryRow(ctx, countQuery, countArgs...).Scan(&total)
+ if err != nil {
+ return nil, 0, err
+ }
+
+ query += " ORDER BY ta.deadline ASC"
+
+ if filters != nil && filters.Limit > 0 {
+ query += fmt.Sprintf(" LIMIT $%d", argIdx)
+ args = append(args, filters.Limit)
+ argIdx++
+ if filters.Offset > 0 {
+ query += fmt.Sprintf(" OFFSET $%d", argIdx)
+ args = append(args, filters.Offset)
+ argIdx++
+ }
+ }
+
+ rows, err := s.pool.Query(ctx, query, args...)
+ if err != nil {
+ return nil, 0, err
+ }
+ defer rows.Close()
+
+ var assignments []TrainingAssignment
+ for rows.Next() {
+ var a TrainingAssignment
+ var status, triggerType string
+
+ err := rows.Scan(
+ &a.ID, &a.TenantID, &a.ModuleID, &a.UserID, &a.UserName, &a.UserEmail,
+ &a.RoleCode, &triggerType, &a.TriggerEvent, &status, &a.ProgressPercent,
+ &a.QuizScore, &a.QuizPassed, &a.QuizAttempts,
+ &a.StartedAt, &a.CompletedAt, &a.Deadline, &a.CertificateID,
+ &a.EscalationLevel, &a.LastEscalationAt, &a.EnrollmentID,
+ &a.CreatedAt, &a.UpdatedAt,
+ &a.ModuleCode, &a.ModuleTitle,
+ )
+ if err != nil {
+ return nil, 0, err
+ }
+
+ a.Status = AssignmentStatus(status)
+ a.TriggerType = TriggerType(triggerType)
+ assignments = append(assignments, a)
+ }
+
+ if assignments == nil {
+ assignments = []TrainingAssignment{}
+ }
+
+ return assignments, total, nil
+}
+
+// UpdateAssignmentStatus updates the status and related fields
+func (s *Store) UpdateAssignmentStatus(ctx context.Context, id uuid.UUID, status AssignmentStatus, progress int) error {
+ now := time.Now().UTC()
+
+ _, err := s.pool.Exec(ctx, `
+ UPDATE training_assignments SET
+ status = $2,
+ progress_percent = $3,
+ started_at = CASE
+ WHEN started_at IS NULL AND $2 IN ('in_progress', 'completed') THEN $4
+ ELSE started_at
+ END,
+ completed_at = CASE
+ WHEN $2 = 'completed' THEN $4
+ ELSE completed_at
+ END,
+ updated_at = $4
+ WHERE id = $1
+ `, id, string(status), progress, now)
+
+ return err
+}
+
+// UpdateAssignmentDeadline updates the deadline of an assignment
+func (s *Store) UpdateAssignmentDeadline(ctx context.Context, id uuid.UUID, deadline time.Time) error {
+ now := time.Now().UTC()
+ _, err := s.pool.Exec(ctx, `
+ UPDATE training_assignments SET
+ deadline = $2,
+ updated_at = $3
+ WHERE id = $1
+ `, id, deadline, now)
+ return err
+}
+
+// UpdateAssignmentQuizResult updates quiz-related fields on an assignment
+func (s *Store) UpdateAssignmentQuizResult(ctx context.Context, id uuid.UUID, score float64, passed bool, attempts int) error {
+ now := time.Now().UTC()
+
+ _, err := s.pool.Exec(ctx, `
+ UPDATE training_assignments SET
+ quiz_score = $2,
+ quiz_passed = $3,
+ quiz_attempts = $4,
+ status = CASE WHEN $3 = true THEN 'completed' ELSE status END,
+ completed_at = CASE WHEN $3 = true THEN $5 ELSE completed_at END,
+ progress_percent = CASE WHEN $3 = true THEN 100 ELSE progress_percent END,
+ updated_at = $5
+ WHERE id = $1
+ `, id, score, passed, attempts, now)
+
+ return err
+}
+
+// ListOverdueAssignments returns assignments past their deadline
+func (s *Store) ListOverdueAssignments(ctx context.Context, tenantID uuid.UUID) ([]TrainingAssignment, error) {
+ overdue := true
+ assignments, _, err := s.ListAssignments(ctx, tenantID, &AssignmentFilters{
+ Overdue: &overdue,
+ Limit: 1000,
+ })
+ return assignments, err
+}
+
+// SetCertificateID sets the certificate ID on an assignment
+func (s *Store) SetCertificateID(ctx context.Context, assignmentID, certID uuid.UUID) error {
+ _, err := s.pool.Exec(ctx, `
+ UPDATE training_assignments SET certificate_id = $2, updated_at = NOW() WHERE id = $1
+ `, assignmentID, certID)
+ return err
+}
+
+// GetAssignmentByCertificateID finds an assignment by its certificate ID
+func (s *Store) GetAssignmentByCertificateID(ctx context.Context, certID uuid.UUID) (*TrainingAssignment, error) {
+ var assignmentID uuid.UUID
+ err := s.pool.QueryRow(ctx,
+ "SELECT id FROM training_assignments WHERE certificate_id = $1",
+ certID).Scan(&assignmentID)
+ if err == pgx.ErrNoRows {
+ return nil, nil
+ }
+ if err != nil {
+ return nil, err
+ }
+ return s.GetAssignment(ctx, assignmentID)
+}
+
+// ListCertificates lists assignments that have certificates for a tenant
+func (s *Store) ListCertificates(ctx context.Context, tenantID uuid.UUID) ([]TrainingAssignment, error) {
+ rows, err := s.pool.Query(ctx, `
+ SELECT
+ ta.id, ta.tenant_id, ta.module_id, ta.user_id, ta.user_name, ta.user_email,
+ ta.role_code, ta.trigger_type, ta.trigger_event, ta.status, ta.progress_percent,
+ ta.quiz_score, ta.quiz_passed, ta.quiz_attempts,
+ ta.started_at, ta.completed_at, ta.deadline, ta.certificate_id,
+ ta.escalation_level, ta.last_escalation_at, ta.enrollment_id,
+ ta.created_at, ta.updated_at,
+ m.module_code, m.title
+ FROM training_assignments ta
+ JOIN training_modules m ON m.id = ta.module_id
+ WHERE ta.tenant_id = $1 AND ta.certificate_id IS NOT NULL
+ ORDER BY ta.completed_at DESC
+ `, tenantID)
+ if err != nil {
+ return nil, err
+ }
+ defer rows.Close()
+
+ var assignments []TrainingAssignment
+ for rows.Next() {
+ var a TrainingAssignment
+ var status, triggerType string
+
+ err := rows.Scan(
+ &a.ID, &a.TenantID, &a.ModuleID, &a.UserID, &a.UserName, &a.UserEmail,
+ &a.RoleCode, &triggerType, &a.TriggerEvent, &status, &a.ProgressPercent,
+ &a.QuizScore, &a.QuizPassed, &a.QuizAttempts,
+ &a.StartedAt, &a.CompletedAt, &a.Deadline, &a.CertificateID,
+ &a.EscalationLevel, &a.LastEscalationAt, &a.EnrollmentID,
+ &a.CreatedAt, &a.UpdatedAt,
+ &a.ModuleCode, &a.ModuleTitle,
+ )
+ if err != nil {
+ return nil, err
+ }
+
+ a.Status = AssignmentStatus(status)
+ a.TriggerType = TriggerType(triggerType)
+ assignments = append(assignments, a)
+ }
+
+ if assignments == nil {
+ assignments = []TrainingAssignment{}
+ }
+
+ return assignments, nil
+}
diff --git a/ai-compliance-sdk/internal/training/store_audit.go b/ai-compliance-sdk/internal/training/store_audit.go
new file mode 100644
index 0000000..c8f690a
--- /dev/null
+++ b/ai-compliance-sdk/internal/training/store_audit.go
@@ -0,0 +1,128 @@
+package training
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "time"
+
+ "github.com/google/uuid"
+)
+
+// LogAction creates an audit log entry
+func (s *Store) LogAction(ctx context.Context, entry *AuditLogEntry) error {
+ entry.ID = uuid.New()
+ entry.CreatedAt = time.Now().UTC()
+
+ details, _ := json.Marshal(entry.Details)
+
+ _, err := s.pool.Exec(ctx, `
+ INSERT INTO training_audit_log (
+ id, tenant_id, user_id, action, entity_type,
+ entity_id, details, ip_address, created_at
+ ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
+ `,
+ entry.ID, entry.TenantID, entry.UserID, string(entry.Action), string(entry.EntityType),
+ entry.EntityID, details, entry.IPAddress, entry.CreatedAt,
+ )
+
+ return err
+}
+
+// ListAuditLog lists audit log entries for a tenant
+func (s *Store) ListAuditLog(ctx context.Context, tenantID uuid.UUID, filters *AuditLogFilters) ([]AuditLogEntry, int, error) {
+ countQuery := "SELECT COUNT(*) FROM training_audit_log WHERE tenant_id = $1"
+ countArgs := []interface{}{tenantID}
+ countArgIdx := 2
+
+ query := `
+ SELECT
+ id, tenant_id, user_id, action, entity_type,
+ entity_id, details, ip_address, created_at
+ FROM training_audit_log WHERE tenant_id = $1`
+
+ args := []interface{}{tenantID}
+ argIdx := 2
+
+ if filters != nil {
+ if filters.UserID != nil {
+ query += fmt.Sprintf(" AND user_id = $%d", argIdx)
+ args = append(args, *filters.UserID)
+ argIdx++
+ countQuery += fmt.Sprintf(" AND user_id = $%d", countArgIdx)
+ countArgs = append(countArgs, *filters.UserID)
+ countArgIdx++
+ }
+ if filters.Action != "" {
+ query += fmt.Sprintf(" AND action = $%d", argIdx)
+ args = append(args, string(filters.Action))
+ argIdx++
+ countQuery += fmt.Sprintf(" AND action = $%d", countArgIdx)
+ countArgs = append(countArgs, string(filters.Action))
+ countArgIdx++
+ }
+ if filters.EntityType != "" {
+ query += fmt.Sprintf(" AND entity_type = $%d", argIdx)
+ args = append(args, string(filters.EntityType))
+ argIdx++
+ countQuery += fmt.Sprintf(" AND entity_type = $%d", countArgIdx)
+ countArgs = append(countArgs, string(filters.EntityType))
+ countArgIdx++
+ }
+ }
+
+ var total int
+ err := s.pool.QueryRow(ctx, countQuery, countArgs...).Scan(&total)
+ if err != nil {
+ return nil, 0, err
+ }
+
+ query += " ORDER BY created_at DESC"
+
+ if filters != nil && filters.Limit > 0 {
+ query += fmt.Sprintf(" LIMIT $%d", argIdx)
+ args = append(args, filters.Limit)
+ argIdx++
+ if filters.Offset > 0 {
+ query += fmt.Sprintf(" OFFSET $%d", argIdx)
+ args = append(args, filters.Offset)
+ argIdx++
+ }
+ }
+
+ rows, err := s.pool.Query(ctx, query, args...)
+ if err != nil {
+ return nil, 0, err
+ }
+ defer rows.Close()
+
+ var entries []AuditLogEntry
+ for rows.Next() {
+ var entry AuditLogEntry
+ var action, entityType string
+ var details []byte
+
+ err := rows.Scan(
+ &entry.ID, &entry.TenantID, &entry.UserID, &action, &entityType,
+ &entry.EntityID, &details, &entry.IPAddress, &entry.CreatedAt,
+ )
+ if err != nil {
+ return nil, 0, err
+ }
+
+ entry.Action = AuditAction(action)
+ entry.EntityType = AuditEntityType(entityType)
+ json.Unmarshal(details, &entry.Details)
+ if entry.Details == nil {
+ entry.Details = map[string]interface{}{}
+ }
+
+ entries = append(entries, entry)
+ }
+
+ if entries == nil {
+ entries = []AuditLogEntry{}
+ }
+
+ return entries, total, nil
+}
diff --git a/ai-compliance-sdk/internal/training/store_checkpoints.go b/ai-compliance-sdk/internal/training/store_checkpoints.go
new file mode 100644
index 0000000..4100319
--- /dev/null
+++ b/ai-compliance-sdk/internal/training/store_checkpoints.go
@@ -0,0 +1,198 @@
+package training
+
+import (
+ "context"
+ "encoding/json"
+ "time"
+
+ "github.com/google/uuid"
+ "github.com/jackc/pgx/v5"
+)
+
+// CreateCheckpoint inserts a new checkpoint
+func (s *Store) CreateCheckpoint(ctx context.Context, cp *Checkpoint) error {
+ cp.ID = uuid.New()
+ cp.CreatedAt = time.Now().UTC()
+
+ _, err := s.pool.Exec(ctx, `
+ INSERT INTO training_checkpoints (id, module_id, checkpoint_index, title, timestamp_seconds, created_at)
+ VALUES ($1, $2, $3, $4, $5, $6)
+ `, cp.ID, cp.ModuleID, cp.CheckpointIndex, cp.Title, cp.TimestampSeconds, cp.CreatedAt)
+
+ return err
+}
+
+// ListCheckpoints returns all checkpoints for a module ordered by index
+func (s *Store) ListCheckpoints(ctx context.Context, moduleID uuid.UUID) ([]Checkpoint, error) {
+ rows, err := s.pool.Query(ctx, `
+ SELECT id, module_id, checkpoint_index, title, timestamp_seconds, created_at
+ FROM training_checkpoints
+ WHERE module_id = $1
+ ORDER BY checkpoint_index
+ `, moduleID)
+ if err != nil {
+ return nil, err
+ }
+ defer rows.Close()
+
+ var checkpoints []Checkpoint
+ for rows.Next() {
+ var cp Checkpoint
+ if err := rows.Scan(&cp.ID, &cp.ModuleID, &cp.CheckpointIndex, &cp.Title, &cp.TimestampSeconds, &cp.CreatedAt); err != nil {
+ return nil, err
+ }
+ checkpoints = append(checkpoints, cp)
+ }
+
+ if checkpoints == nil {
+ checkpoints = []Checkpoint{}
+ }
+ return checkpoints, nil
+}
+
+// DeleteCheckpointsForModule removes all checkpoints for a module (used before regenerating)
+func (s *Store) DeleteCheckpointsForModule(ctx context.Context, moduleID uuid.UUID) error {
+ _, err := s.pool.Exec(ctx, `DELETE FROM training_checkpoints WHERE module_id = $1`, moduleID)
+ return err
+}
+
+// GetCheckpointProgress retrieves progress for a specific checkpoint+assignment
+func (s *Store) GetCheckpointProgress(ctx context.Context, assignmentID, checkpointID uuid.UUID) (*CheckpointProgress, error) {
+ var cp CheckpointProgress
+ err := s.pool.QueryRow(ctx, `
+ SELECT id, assignment_id, checkpoint_id, passed, attempts, last_attempt_at, created_at
+ FROM training_checkpoint_progress
+ WHERE assignment_id = $1 AND checkpoint_id = $2
+ `, assignmentID, checkpointID).Scan(
+ &cp.ID, &cp.AssignmentID, &cp.CheckpointID, &cp.Passed, &cp.Attempts, &cp.LastAttemptAt, &cp.CreatedAt,
+ )
+ if err == pgx.ErrNoRows {
+ return nil, nil
+ }
+ if err != nil {
+ return nil, err
+ }
+ return &cp, nil
+}
+
+// UpsertCheckpointProgress creates or updates checkpoint progress
+func (s *Store) UpsertCheckpointProgress(ctx context.Context, progress *CheckpointProgress) error {
+ progress.ID = uuid.New()
+ now := time.Now().UTC()
+ progress.LastAttemptAt = &now
+ progress.CreatedAt = now
+
+ _, err := s.pool.Exec(ctx, `
+ INSERT INTO training_checkpoint_progress (id, assignment_id, checkpoint_id, passed, attempts, last_attempt_at, created_at)
+ VALUES ($1, $2, $3, $4, $5, $6, $7)
+ ON CONFLICT (assignment_id, checkpoint_id) DO UPDATE SET
+ passed = EXCLUDED.passed,
+ attempts = training_checkpoint_progress.attempts + 1,
+ last_attempt_at = EXCLUDED.last_attempt_at
+ `, progress.ID, progress.AssignmentID, progress.CheckpointID, progress.Passed, progress.Attempts, progress.LastAttemptAt, progress.CreatedAt)
+
+ return err
+}
+
+// GetCheckpointQuestions retrieves quiz questions for a specific checkpoint
+func (s *Store) GetCheckpointQuestions(ctx context.Context, checkpointID uuid.UUID) ([]QuizQuestion, error) {
+ rows, err := s.pool.Query(ctx, `
+ SELECT id, module_id, question, options, correct_index, explanation, difficulty, is_active, sort_order, created_at
+ FROM training_quiz_questions
+ WHERE checkpoint_id = $1 AND is_active = true
+ ORDER BY sort_order
+ `, checkpointID)
+ if err != nil {
+ return nil, err
+ }
+ defer rows.Close()
+
+ var questions []QuizQuestion
+ for rows.Next() {
+ var q QuizQuestion
+ var options []byte
+ var difficulty string
+ if err := rows.Scan(&q.ID, &q.ModuleID, &q.Question, &options, &q.CorrectIndex, &q.Explanation, &difficulty, &q.IsActive, &q.SortOrder, &q.CreatedAt); err != nil {
+ return nil, err
+ }
+ json.Unmarshal(options, &q.Options)
+ q.Difficulty = Difficulty(difficulty)
+ questions = append(questions, q)
+ }
+
+ if questions == nil {
+ questions = []QuizQuestion{}
+ }
+ return questions, nil
+}
+
+// CreateCheckpointQuizQuestion creates a quiz question linked to a checkpoint
+func (s *Store) CreateCheckpointQuizQuestion(ctx context.Context, q *QuizQuestion, checkpointID uuid.UUID) error {
+ q.ID = uuid.New()
+ q.CreatedAt = time.Now().UTC()
+ q.IsActive = true
+
+ options, _ := json.Marshal(q.Options)
+
+ _, err := s.pool.Exec(ctx, `
+ INSERT INTO training_quiz_questions (id, module_id, checkpoint_id, question, options, correct_index, explanation, difficulty, is_active, sort_order, created_at)
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
+ `, q.ID, q.ModuleID, checkpointID, q.Question, options, q.CorrectIndex, q.Explanation, string(q.Difficulty), q.IsActive, q.SortOrder, q.CreatedAt)
+
+ return err
+}
+
+// AreAllCheckpointsPassed checks if all checkpoints for a module are passed by an assignment
+func (s *Store) AreAllCheckpointsPassed(ctx context.Context, assignmentID, moduleID uuid.UUID) (bool, error) {
+ var totalCheckpoints, passedCheckpoints int
+
+ err := s.pool.QueryRow(ctx, `
+ SELECT COUNT(*) FROM training_checkpoints WHERE module_id = $1
+ `, moduleID).Scan(&totalCheckpoints)
+ if err != nil {
+ return false, err
+ }
+
+ if totalCheckpoints == 0 {
+ return true, nil
+ }
+
+ err = s.pool.QueryRow(ctx, `
+ SELECT COUNT(*) FROM training_checkpoint_progress cp
+ JOIN training_checkpoints c ON cp.checkpoint_id = c.id
+ WHERE cp.assignment_id = $1 AND c.module_id = $2 AND cp.passed = true
+ `, assignmentID, moduleID).Scan(&passedCheckpoints)
+ if err != nil {
+ return false, err
+ }
+
+ return passedCheckpoints >= totalCheckpoints, nil
+}
+
+// ListCheckpointProgress returns all checkpoint progress for an assignment
+func (s *Store) ListCheckpointProgress(ctx context.Context, assignmentID uuid.UUID) ([]CheckpointProgress, error) {
+ rows, err := s.pool.Query(ctx, `
+ SELECT id, assignment_id, checkpoint_id, passed, attempts, last_attempt_at, created_at
+ FROM training_checkpoint_progress
+ WHERE assignment_id = $1
+ ORDER BY created_at
+ `, assignmentID)
+ if err != nil {
+ return nil, err
+ }
+ defer rows.Close()
+
+ var progress []CheckpointProgress
+ for rows.Next() {
+ var cp CheckpointProgress
+ if err := rows.Scan(&cp.ID, &cp.AssignmentID, &cp.CheckpointID, &cp.Passed, &cp.Attempts, &cp.LastAttemptAt, &cp.CreatedAt); err != nil {
+ return nil, err
+ }
+ progress = append(progress, cp)
+ }
+
+ if progress == nil {
+ progress = []CheckpointProgress{}
+ }
+ return progress, nil
+}
diff --git a/ai-compliance-sdk/internal/training/store_content.go b/ai-compliance-sdk/internal/training/store_content.go
new file mode 100644
index 0000000..067cd8f
--- /dev/null
+++ b/ai-compliance-sdk/internal/training/store_content.go
@@ -0,0 +1,130 @@
+package training
+
+import (
+ "context"
+ "time"
+
+ "github.com/google/uuid"
+ "github.com/jackc/pgx/v5"
+)
+
+// CreateModuleContent creates new content for a module
+func (s *Store) CreateModuleContent(ctx context.Context, content *ModuleContent) error {
+ content.ID = uuid.New()
+ content.CreatedAt = time.Now().UTC()
+ content.UpdatedAt = content.CreatedAt
+
+ // Auto-increment version
+ var maxVersion int
+ s.pool.QueryRow(ctx,
+ "SELECT COALESCE(MAX(version), 0) FROM training_module_content WHERE module_id = $1",
+ content.ModuleID).Scan(&maxVersion)
+ content.Version = maxVersion + 1
+
+ _, err := s.pool.Exec(ctx, `
+ INSERT INTO training_module_content (
+ id, module_id, version, content_format, content_body,
+ summary, generated_by, llm_model, is_published,
+ reviewed_by, reviewed_at, created_at, updated_at
+ ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)
+ `,
+ content.ID, content.ModuleID, content.Version, string(content.ContentFormat), content.ContentBody,
+ content.Summary, content.GeneratedBy, content.LLMModel, content.IsPublished,
+ content.ReviewedBy, content.ReviewedAt, content.CreatedAt, content.UpdatedAt,
+ )
+
+ return err
+}
+
+// GetPublishedContent retrieves the published content for a module
+func (s *Store) GetPublishedContent(ctx context.Context, moduleID uuid.UUID) (*ModuleContent, error) {
+ var content ModuleContent
+ var contentFormat string
+
+ err := s.pool.QueryRow(ctx, `
+ SELECT
+ id, module_id, version, content_format, content_body,
+ summary, generated_by, llm_model, is_published,
+ reviewed_by, reviewed_at, created_at, updated_at
+ FROM training_module_content
+ WHERE module_id = $1 AND is_published = true
+ ORDER BY version DESC
+ LIMIT 1
+ `, moduleID).Scan(
+ &content.ID, &content.ModuleID, &content.Version, &contentFormat, &content.ContentBody,
+ &content.Summary, &content.GeneratedBy, &content.LLMModel, &content.IsPublished,
+ &content.ReviewedBy, &content.ReviewedAt, &content.CreatedAt, &content.UpdatedAt,
+ )
+
+ if err == pgx.ErrNoRows {
+ return nil, nil
+ }
+ if err != nil {
+ return nil, err
+ }
+
+ content.ContentFormat = ContentFormat(contentFormat)
+ return &content, nil
+}
+
+// GetLatestContent retrieves the latest content (published or not) for a module
+func (s *Store) GetLatestContent(ctx context.Context, moduleID uuid.UUID) (*ModuleContent, error) {
+ var content ModuleContent
+ var contentFormat string
+
+ err := s.pool.QueryRow(ctx, `
+ SELECT
+ id, module_id, version, content_format, content_body,
+ summary, generated_by, llm_model, is_published,
+ reviewed_by, reviewed_at, created_at, updated_at
+ FROM training_module_content
+ WHERE module_id = $1
+ ORDER BY version DESC
+ LIMIT 1
+ `, moduleID).Scan(
+ &content.ID, &content.ModuleID, &content.Version, &contentFormat, &content.ContentBody,
+ &content.Summary, &content.GeneratedBy, &content.LLMModel, &content.IsPublished,
+ &content.ReviewedBy, &content.ReviewedAt, &content.CreatedAt, &content.UpdatedAt,
+ )
+
+ if err == pgx.ErrNoRows {
+ return nil, nil
+ }
+ if err != nil {
+ return nil, err
+ }
+
+ content.ContentFormat = ContentFormat(contentFormat)
+ return &content, nil
+}
+
+// PublishContent marks a content version as published (unpublishes all others for that module)
+func (s *Store) PublishContent(ctx context.Context, contentID uuid.UUID, reviewedBy uuid.UUID) error {
+ now := time.Now().UTC()
+
+ // Get module_id for this content
+ var moduleID uuid.UUID
+ err := s.pool.QueryRow(ctx,
+ "SELECT module_id FROM training_module_content WHERE id = $1",
+ contentID).Scan(&moduleID)
+ if err != nil {
+ return err
+ }
+
+ // Unpublish all existing content for this module
+ _, err = s.pool.Exec(ctx,
+ "UPDATE training_module_content SET is_published = false WHERE module_id = $1",
+ moduleID)
+ if err != nil {
+ return err
+ }
+
+ // Publish the specified content
+ _, err = s.pool.Exec(ctx, `
+ UPDATE training_module_content SET
+ is_published = true, reviewed_by = $2, reviewed_at = $3, updated_at = $3
+ WHERE id = $1
+ `, contentID, reviewedBy, now)
+
+ return err
+}
diff --git a/ai-compliance-sdk/internal/training/store_matrix.go b/ai-compliance-sdk/internal/training/store_matrix.go
new file mode 100644
index 0000000..5605b7b
--- /dev/null
+++ b/ai-compliance-sdk/internal/training/store_matrix.go
@@ -0,0 +1,112 @@
+package training
+
+import (
+ "context"
+ "time"
+
+ "github.com/google/uuid"
+)
+
+// GetMatrixForRole returns all matrix entries for a given role
+func (s *Store) GetMatrixForRole(ctx context.Context, tenantID uuid.UUID, roleCode string) ([]TrainingMatrixEntry, error) {
+ rows, err := s.pool.Query(ctx, `
+ SELECT
+ tm.id, tm.tenant_id, tm.role_code, tm.module_id,
+ tm.is_mandatory, tm.priority, tm.created_at,
+ m.module_code, m.title
+ FROM training_matrix tm
+ JOIN training_modules m ON m.id = tm.module_id
+ WHERE tm.tenant_id = $1 AND tm.role_code = $2
+ ORDER BY tm.priority ASC
+ `, tenantID, roleCode)
+ if err != nil {
+ return nil, err
+ }
+ defer rows.Close()
+
+ var entries []TrainingMatrixEntry
+ for rows.Next() {
+ var entry TrainingMatrixEntry
+ err := rows.Scan(
+ &entry.ID, &entry.TenantID, &entry.RoleCode, &entry.ModuleID,
+ &entry.IsMandatory, &entry.Priority, &entry.CreatedAt,
+ &entry.ModuleCode, &entry.ModuleTitle,
+ )
+ if err != nil {
+ return nil, err
+ }
+ entries = append(entries, entry)
+ }
+
+ if entries == nil {
+ entries = []TrainingMatrixEntry{}
+ }
+
+ return entries, nil
+}
+
+// GetMatrixForTenant returns the full CTM for a tenant
+func (s *Store) GetMatrixForTenant(ctx context.Context, tenantID uuid.UUID) ([]TrainingMatrixEntry, error) {
+ rows, err := s.pool.Query(ctx, `
+ SELECT
+ tm.id, tm.tenant_id, tm.role_code, tm.module_id,
+ tm.is_mandatory, tm.priority, tm.created_at,
+ m.module_code, m.title
+ FROM training_matrix tm
+ JOIN training_modules m ON m.id = tm.module_id
+ WHERE tm.tenant_id = $1
+ ORDER BY tm.role_code ASC, tm.priority ASC
+ `, tenantID)
+ if err != nil {
+ return nil, err
+ }
+ defer rows.Close()
+
+ var entries []TrainingMatrixEntry
+ for rows.Next() {
+ var entry TrainingMatrixEntry
+ err := rows.Scan(
+ &entry.ID, &entry.TenantID, &entry.RoleCode, &entry.ModuleID,
+ &entry.IsMandatory, &entry.Priority, &entry.CreatedAt,
+ &entry.ModuleCode, &entry.ModuleTitle,
+ )
+ if err != nil {
+ return nil, err
+ }
+ entries = append(entries, entry)
+ }
+
+ if entries == nil {
+ entries = []TrainingMatrixEntry{}
+ }
+
+ return entries, nil
+}
+
+// SetMatrixEntry creates or updates a CTM entry
+func (s *Store) SetMatrixEntry(ctx context.Context, entry *TrainingMatrixEntry) error {
+ entry.ID = uuid.New()
+ entry.CreatedAt = time.Now().UTC()
+
+ _, err := s.pool.Exec(ctx, `
+ INSERT INTO training_matrix (
+ id, tenant_id, role_code, module_id, is_mandatory, priority, created_at
+ ) VALUES ($1, $2, $3, $4, $5, $6, $7)
+ ON CONFLICT (tenant_id, role_code, module_id)
+ DO UPDATE SET is_mandatory = EXCLUDED.is_mandatory, priority = EXCLUDED.priority
+ `,
+ entry.ID, entry.TenantID, entry.RoleCode, entry.ModuleID,
+ entry.IsMandatory, entry.Priority, entry.CreatedAt,
+ )
+
+ return err
+}
+
+// DeleteMatrixEntry removes a CTM entry
+func (s *Store) DeleteMatrixEntry(ctx context.Context, tenantID uuid.UUID, roleCode string, moduleID uuid.UUID) error {
+ _, err := s.pool.Exec(ctx,
+ "DELETE FROM training_matrix WHERE tenant_id = $1 AND role_code = $2 AND module_id = $3",
+ tenantID, roleCode, moduleID,
+ )
+ return err
+}
diff --git a/ai-compliance-sdk/internal/training/store_media.go b/ai-compliance-sdk/internal/training/store_media.go
new file mode 100644
index 0000000..6646092
--- /dev/null
+++ b/ai-compliance-sdk/internal/training/store_media.go
@@ -0,0 +1,192 @@
+package training
+
+import (
+ "context"
+ "encoding/json"
+ "time"
+
+ "github.com/google/uuid"
+ "github.com/jackc/pgx/v5"
+)
+
+// CreateMedia creates a new media record
+func (s *Store) CreateMedia(ctx context.Context, media *TrainingMedia) error {
+ media.ID = uuid.New()
+ media.CreatedAt = time.Now().UTC()
+ media.UpdatedAt = media.CreatedAt
+ if media.Metadata == nil {
+ media.Metadata = json.RawMessage("{}")
+ }
+
+ _, err := s.pool.Exec(ctx, `
+ INSERT INTO training_media (
+ id, module_id, content_id, media_type, status,
+ bucket, object_key, file_size_bytes, duration_seconds,
+ mime_type, voice_model, language, metadata,
+ error_message, generated_by, is_published, created_at, updated_at
+ ) VALUES (
+ $1, $2, $3, $4, $5,
+ $6, $7, $8, $9,
+ $10, $11, $12, $13,
+ $14, $15, $16, $17, $18
+ )
+ `,
+ media.ID, media.ModuleID, media.ContentID, string(media.MediaType), string(media.Status),
+ media.Bucket, media.ObjectKey, media.FileSizeBytes, media.DurationSeconds,
+ media.MimeType, media.VoiceModel, media.Language, media.Metadata,
+ media.ErrorMessage, media.GeneratedBy, media.IsPublished, media.CreatedAt, media.UpdatedAt,
+ )
+
+ return err
+}
+
+// GetMedia retrieves a media record by ID
+func (s *Store) GetMedia(ctx context.Context, id uuid.UUID) (*TrainingMedia, error) {
+ var media TrainingMedia
+ var mediaType, status string
+
+ err := s.pool.QueryRow(ctx, `
+ SELECT id, module_id, content_id, media_type, status,
+ bucket, object_key, file_size_bytes, duration_seconds,
+ mime_type, voice_model, language, metadata,
+ error_message, generated_by, is_published, created_at, updated_at
+ FROM training_media WHERE id = $1
+ `, id).Scan(
+ &media.ID, &media.ModuleID, &media.ContentID, &mediaType, &status,
+ &media.Bucket, &media.ObjectKey, &media.FileSizeBytes, &media.DurationSeconds,
+ &media.MimeType, &media.VoiceModel, &media.Language, &media.Metadata,
+ &media.ErrorMessage, &media.GeneratedBy, &media.IsPublished, &media.CreatedAt, &media.UpdatedAt,
+ )
+
+ if err == pgx.ErrNoRows {
+ return nil, nil
+ }
+ if err != nil {
+ return nil, err
+ }
+
+ media.MediaType = MediaType(mediaType)
+ media.Status = MediaStatus(status)
+ return &media, nil
+}
+
+// GetMediaForModule retrieves all media for a module
+func (s *Store) GetMediaForModule(ctx context.Context, moduleID uuid.UUID) ([]TrainingMedia, error) {
+ rows, err := s.pool.Query(ctx, `
+ SELECT id, module_id, content_id, media_type, status,
+ bucket, object_key, file_size_bytes, duration_seconds,
+ mime_type, voice_model, language, metadata,
+ error_message, generated_by, is_published, created_at, updated_at
+ FROM training_media WHERE module_id = $1
+ ORDER BY media_type, created_at DESC
+ `, moduleID)
+ if err != nil {
+ return nil, err
+ }
+ defer rows.Close()
+
+ var mediaList []TrainingMedia
+ for rows.Next() {
+ var media TrainingMedia
+ var mediaType, status string
+ if err := rows.Scan(
+ &media.ID, &media.ModuleID, &media.ContentID, &mediaType, &status,
+ &media.Bucket, &media.ObjectKey, &media.FileSizeBytes, &media.DurationSeconds,
+ &media.MimeType, &media.VoiceModel, &media.Language, &media.Metadata,
+ &media.ErrorMessage, &media.GeneratedBy, &media.IsPublished, &media.CreatedAt, &media.UpdatedAt,
+ ); err != nil {
+ return nil, err
+ }
+ media.MediaType = MediaType(mediaType)
+ media.Status = MediaStatus(status)
+ mediaList = append(mediaList, media)
+ }
+
+ if mediaList == nil {
+ mediaList = []TrainingMedia{}
+ }
+ return mediaList, nil
+}
+
+// UpdateMediaStatus updates the status and related fields of a media record
+func (s *Store) UpdateMediaStatus(ctx context.Context, id uuid.UUID, status MediaStatus, sizeBytes int64, duration float64, errMsg string) error {
+ _, err := s.pool.Exec(ctx, `
+ UPDATE training_media
+ SET status = $2, file_size_bytes = $3, duration_seconds = $4,
+ error_message = $5, updated_at = NOW()
+ WHERE id = $1
+ `, id, string(status), sizeBytes, duration, errMsg)
+ return err
+}
+
+// PublishMedia publishes or unpublishes a media record
+func (s *Store) PublishMedia(ctx context.Context, id uuid.UUID, publish bool) error {
+ _, err := s.pool.Exec(ctx, `
+ UPDATE training_media SET is_published = $2, updated_at = NOW() WHERE id = $1
+ `, id, publish)
+ return err
+}
+
+// GetPublishedAudio gets the published audio for a module
+func (s *Store) GetPublishedAudio(ctx context.Context, moduleID uuid.UUID) (*TrainingMedia, error) {
+ var media TrainingMedia
+ var mediaType, status string
+
+ err := s.pool.QueryRow(ctx, `
+ SELECT id, module_id, content_id, media_type, status,
+ bucket, object_key, file_size_bytes, duration_seconds,
+ mime_type, voice_model, language, metadata,
+ error_message, generated_by, is_published, created_at, updated_at
+ FROM training_media
+ WHERE module_id = $1 AND media_type = 'audio' AND is_published = true
+ ORDER BY created_at DESC LIMIT 1
+ `, moduleID).Scan(
+ &media.ID, &media.ModuleID, &media.ContentID, &mediaType, &status,
+ &media.Bucket, &media.ObjectKey, &media.FileSizeBytes, &media.DurationSeconds,
+ &media.MimeType, &media.VoiceModel, &media.Language, &media.Metadata,
+ &media.ErrorMessage, &media.GeneratedBy, &media.IsPublished, &media.CreatedAt, &media.UpdatedAt,
+ )
+
+ if err == pgx.ErrNoRows {
+ return nil, nil
+ }
+ if err != nil {
+ return nil, err
+ }
+
+ media.MediaType = MediaType(mediaType)
+ media.Status = MediaStatus(status)
+ return &media, nil
+}
+
+// GetPublishedVideo gets the published video for a module
+func (s *Store) GetPublishedVideo(ctx context.Context, moduleID uuid.UUID) (*TrainingMedia, error) {
+ var media TrainingMedia
+ var mediaType, status string
+
+ err := s.pool.QueryRow(ctx, `
+ SELECT id, module_id, content_id, media_type, status,
+ bucket, object_key, file_size_bytes, duration_seconds,
+ mime_type, voice_model, language, metadata,
+ error_message, generated_by, is_published, created_at, updated_at
+ FROM training_media
+ WHERE module_id = $1 AND media_type = 'video' AND is_published = true
+ ORDER BY created_at DESC LIMIT 1
+ `, moduleID).Scan(
+ &media.ID, &media.ModuleID, &media.ContentID, &mediaType, &status,
+ &media.Bucket, &media.ObjectKey, &media.FileSizeBytes, &media.DurationSeconds,
+ &media.MimeType, &media.VoiceModel, &media.Language, &media.Metadata,
+ &media.ErrorMessage, &media.GeneratedBy, &media.IsPublished, &media.CreatedAt, &media.UpdatedAt,
+ )
+
+ if err == pgx.ErrNoRows {
+ return nil, nil
+ }
+ if err != nil {
+ return nil, err
+ }
+
+ media.MediaType = MediaType(mediaType)
+ media.Status = MediaStatus(status)
+ return &media, nil
+}
diff --git a/ai-compliance-sdk/internal/training/store_modules.go b/ai-compliance-sdk/internal/training/store_modules.go
new file mode 100644
index 0000000..0213b3a
--- /dev/null
+++ b/ai-compliance-sdk/internal/training/store_modules.go
@@ -0,0 +1,235 @@
+package training
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "time"
+
+ "github.com/google/uuid"
+ "github.com/jackc/pgx/v5"
+)
+
+// CreateModule creates a new training module
+func (s *Store) CreateModule(ctx context.Context, module *TrainingModule) error {
+ module.ID = uuid.New()
+ module.CreatedAt = time.Now().UTC()
+ module.UpdatedAt = module.CreatedAt
+ if !module.IsActive {
+ module.IsActive = true
+ }
+
+ isoControls, _ := json.Marshal(module.ISOControls)
+
+ _, err := s.pool.Exec(ctx, `
+ INSERT INTO training_modules (
+ id, tenant_id, academy_course_id, module_code, title, description,
+ regulation_area, nis2_relevant, iso_controls, frequency_type,
+ validity_days, risk_weight, content_type, duration_minutes,
+ pass_threshold, is_active, sort_order, created_at, updated_at
+ ) VALUES (
+ $1, $2, $3, $4, $5, $6,
+ $7, $8, $9, $10,
+ $11, $12, $13, $14,
+ $15, $16, $17, $18, $19
+ )
+ `,
+ module.ID, module.TenantID, module.AcademyCourseID, module.ModuleCode, module.Title, module.Description,
+ string(module.RegulationArea), module.NIS2Relevant, isoControls, string(module.FrequencyType),
+ module.ValidityDays, module.RiskWeight, module.ContentType, module.DurationMinutes,
+ module.PassThreshold, module.IsActive, module.SortOrder, module.CreatedAt, module.UpdatedAt,
+ )
+
+ return err
+}
+
+// GetModule retrieves a module by ID
+func (s *Store) GetModule(ctx context.Context, id uuid.UUID) (*TrainingModule, error) {
+ var module TrainingModule
+ var regulationArea, frequencyType string
+ var isoControls []byte
+
+ err := s.pool.QueryRow(ctx, `
+ SELECT
+ id, tenant_id, academy_course_id, module_code, title, description,
+ regulation_area, nis2_relevant, iso_controls, frequency_type,
+ validity_days, risk_weight, content_type, duration_minutes,
+ pass_threshold, is_active, sort_order, created_at, updated_at
+ FROM training_modules WHERE id = $1
+ `, id).Scan(
+ &module.ID, &module.TenantID, &module.AcademyCourseID, &module.ModuleCode, &module.Title, &module.Description,
+ ®ulationArea, &module.NIS2Relevant, &isoControls, &frequencyType,
+ &module.ValidityDays, &module.RiskWeight, &module.ContentType, &module.DurationMinutes,
+ &module.PassThreshold, &module.IsActive, &module.SortOrder, &module.CreatedAt, &module.UpdatedAt,
+ )
+
+ if err == pgx.ErrNoRows {
+ return nil, nil
+ }
+ if err != nil {
+ return nil, err
+ }
+
+ module.RegulationArea = RegulationArea(regulationArea)
+ module.FrequencyType = FrequencyType(frequencyType)
+ json.Unmarshal(isoControls, &module.ISOControls)
+ if module.ISOControls == nil {
+ module.ISOControls = []string{}
+ }
+
+ return &module, nil
+}
+
+// ListModules lists training modules for a tenant with optional filters
+func (s *Store) ListModules(ctx context.Context, tenantID uuid.UUID, filters *ModuleFilters) ([]TrainingModule, int, error) {
+ countQuery := "SELECT COUNT(*) FROM training_modules WHERE tenant_id = $1"
+ countArgs := []interface{}{tenantID}
+ countArgIdx := 2
+
+ query := `
+ SELECT
+ id, tenant_id, academy_course_id, module_code, title, description,
+ regulation_area, nis2_relevant, iso_controls, frequency_type,
+ validity_days, risk_weight, content_type, duration_minutes,
+ pass_threshold, is_active, sort_order, created_at, updated_at
+ FROM training_modules WHERE tenant_id = $1`
+
+ args := []interface{}{tenantID}
+ argIdx := 2
+
+ if filters != nil {
+ if filters.RegulationArea != "" {
+ query += fmt.Sprintf(" AND regulation_area = $%d", argIdx)
+ args = append(args, string(filters.RegulationArea))
+ argIdx++
+ countQuery += fmt.Sprintf(" AND regulation_area = $%d", countArgIdx)
+ countArgs = append(countArgs, string(filters.RegulationArea))
+ countArgIdx++
+ }
+ if filters.FrequencyType != "" {
+ query += fmt.Sprintf(" AND frequency_type = $%d", argIdx)
+ args = append(args, string(filters.FrequencyType))
+ argIdx++
+ countQuery += fmt.Sprintf(" AND frequency_type = $%d", countArgIdx)
+ countArgs = append(countArgs, string(filters.FrequencyType))
+ countArgIdx++
+ }
+ if filters.IsActive != nil {
+ query += fmt.Sprintf(" AND is_active = $%d", argIdx)
+ args = append(args, *filters.IsActive)
+ argIdx++
+ countQuery += fmt.Sprintf(" AND is_active = $%d", countArgIdx)
+ countArgs = append(countArgs, *filters.IsActive)
+ countArgIdx++
+ }
+ if filters.NIS2Relevant != nil {
+ query += fmt.Sprintf(" AND nis2_relevant = $%d", argIdx)
+ args = append(args, *filters.NIS2Relevant)
+ argIdx++
+ countQuery += fmt.Sprintf(" AND nis2_relevant = $%d", countArgIdx)
+ countArgs = append(countArgs, *filters.NIS2Relevant)
+ countArgIdx++
+ }
+ if filters.Search != "" {
+ query += fmt.Sprintf(" AND (title ILIKE $%d OR description ILIKE $%d OR module_code ILIKE $%d)", argIdx, argIdx, argIdx)
+ args = append(args, "%"+filters.Search+"%")
+ argIdx++
+ countQuery += fmt.Sprintf(" AND (title ILIKE $%d OR description ILIKE $%d OR module_code ILIKE $%d)", countArgIdx, countArgIdx, countArgIdx)
+ countArgs = append(countArgs, "%"+filters.Search+"%")
+ countArgIdx++
+ }
+ }
+
+ var total int
+ err := s.pool.QueryRow(ctx, countQuery, countArgs...).Scan(&total)
+ if err != nil {
+ return nil, 0, err
+ }
+
+ query += " ORDER BY sort_order ASC, created_at DESC"
+
+ if filters != nil && filters.Limit > 0 {
+ query += fmt.Sprintf(" LIMIT $%d", argIdx)
+ args = append(args, filters.Limit)
+ argIdx++
+ if filters.Offset > 0 {
+ query += fmt.Sprintf(" OFFSET $%d", argIdx)
+ args = append(args, filters.Offset)
+ argIdx++
+ }
+ }
+
+ rows, err := s.pool.Query(ctx, query, args...)
+ if err != nil {
+ return nil, 0, err
+ }
+ defer rows.Close()
+
+ var modules []TrainingModule
+ for rows.Next() {
+ var module TrainingModule
+ var regulationArea, frequencyType string
+ var isoControls []byte
+
+ err := rows.Scan(
+ &module.ID, &module.TenantID, &module.AcademyCourseID, &module.ModuleCode, &module.Title, &module.Description,
+ ®ulationArea, &module.NIS2Relevant, &isoControls, &frequencyType,
+ &module.ValidityDays, &module.RiskWeight, &module.ContentType, &module.DurationMinutes,
+ &module.PassThreshold, &module.IsActive, &module.SortOrder, &module.CreatedAt, &module.UpdatedAt,
+ )
+ if err != nil {
+ return nil, 0, err
+ }
+
+ module.RegulationArea = RegulationArea(regulationArea)
+ module.FrequencyType = FrequencyType(frequencyType)
+ json.Unmarshal(isoControls, &module.ISOControls)
+ if module.ISOControls == nil {
+ module.ISOControls = []string{}
+ }
+
+ modules = append(modules, module)
+ }
+
+ if modules == nil {
+ modules = []TrainingModule{}
+ }
+
+ return modules, total, nil
+}
+
+// UpdateModule updates a training module
+func (s *Store) UpdateModule(ctx context.Context, module *TrainingModule) error {
+ module.UpdatedAt = time.Now().UTC()
+ isoControls, _ := json.Marshal(module.ISOControls)
+
+ _, err := s.pool.Exec(ctx, `
+ UPDATE training_modules SET
+ title = $2, description = $3, nis2_relevant = $4,
+ iso_controls = $5, validity_days = $6, risk_weight = $7,
+ duration_minutes = $8, pass_threshold = $9, is_active = $10,
+ sort_order = $11, updated_at = $12
+ WHERE id = $1
+ `,
+ module.ID, module.Title, module.Description, module.NIS2Relevant,
+ isoControls, module.ValidityDays, module.RiskWeight,
+ module.DurationMinutes, module.PassThreshold, module.IsActive,
+ module.SortOrder, module.UpdatedAt,
+ )
+
+ return err
+}
+
+// DeleteModule deletes a training module by ID
+func (s *Store) DeleteModule(ctx context.Context, id uuid.UUID) error {
+ _, err := s.pool.Exec(ctx, `DELETE FROM training_modules WHERE id = $1`, id)
+ return err
+}
+
+// SetAcademyCourseID links a training module to an academy course
+func (s *Store) SetAcademyCourseID(ctx context.Context, moduleID, courseID uuid.UUID) error {
+ _, err := s.pool.Exec(ctx, `
+ UPDATE training_modules SET academy_course_id = $2, updated_at = $3 WHERE id = $1
+ `, moduleID, courseID, time.Now().UTC())
+ return err
+}
diff --git a/ai-compliance-sdk/internal/training/store_quiz.go b/ai-compliance-sdk/internal/training/store_quiz.go
new file mode 100644
index 0000000..022465c
--- /dev/null
+++ b/ai-compliance-sdk/internal/training/store_quiz.go
@@ -0,0 +1,140 @@
+package training
+
+import (
+ "context"
+ "encoding/json"
+ "time"
+
+ "github.com/google/uuid"
+)
+
+// CreateQuizQuestion creates a new quiz question
+func (s *Store) CreateQuizQuestion(ctx context.Context, q *QuizQuestion) error {
+ q.ID = uuid.New()
+ q.CreatedAt = time.Now().UTC()
+ if !q.IsActive {
+ q.IsActive = true
+ }
+
+ options, _ := json.Marshal(q.Options)
+
+ _, err := s.pool.Exec(ctx, `
+ INSERT INTO training_quiz_questions (
+ id, module_id, question, options, correct_index,
+ explanation, difficulty, is_active, sort_order, created_at
+ ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
+ `,
+ q.ID, q.ModuleID, q.Question, options, q.CorrectIndex,
+ q.Explanation, string(q.Difficulty), q.IsActive, q.SortOrder, q.CreatedAt,
+ )
+
+ return err
+}
+
+// ListQuizQuestions lists quiz questions for a module
+func (s *Store) ListQuizQuestions(ctx context.Context, moduleID uuid.UUID) ([]QuizQuestion, error) {
+ rows, err := s.pool.Query(ctx, `
+ SELECT
+ id, module_id, question, options, correct_index,
+ explanation, difficulty, is_active, sort_order, created_at
+ FROM training_quiz_questions
+ WHERE module_id = $1 AND is_active = true
+ ORDER BY sort_order ASC, created_at ASC
+ `, moduleID)
+ if err != nil {
+ return nil, err
+ }
+ defer rows.Close()
+
+ var questions []QuizQuestion
+ for rows.Next() {
+ var q QuizQuestion
+ var options []byte
+ var difficulty string
+
+ err := rows.Scan(
+ &q.ID, &q.ModuleID, &q.Question, &options, &q.CorrectIndex,
+ &q.Explanation, &difficulty, &q.IsActive, &q.SortOrder, &q.CreatedAt,
+ )
+ if err != nil {
+ return nil, err
+ }
+
+ q.Difficulty = Difficulty(difficulty)
+ json.Unmarshal(options, &q.Options)
+ if q.Options == nil {
+ q.Options = []string{}
+ }
+
+ questions = append(questions, q)
+ }
+
+ if questions == nil {
+ questions = []QuizQuestion{}
+ }
+
+ return questions, nil
+}
+
+// CreateQuizAttempt records a quiz attempt
+func (s *Store) CreateQuizAttempt(ctx context.Context, attempt *QuizAttempt) error {
+ attempt.ID = uuid.New()
+ attempt.AttemptedAt = time.Now().UTC()
+
+ answers, _ := json.Marshal(attempt.Answers)
+
+ _, err := s.pool.Exec(ctx, `
+ INSERT INTO training_quiz_attempts (
+ id, assignment_id, user_id, answers, score,
+ passed, correct_count, total_count, duration_seconds, attempted_at
+ ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
+ `,
+ attempt.ID, attempt.AssignmentID, attempt.UserID, answers, attempt.Score,
+ attempt.Passed, attempt.CorrectCount, attempt.TotalCount, attempt.DurationSeconds, attempt.AttemptedAt,
+ )
+
+ return err
+}
+
+// ListQuizAttempts lists quiz attempts for an assignment
+func (s *Store) ListQuizAttempts(ctx context.Context, assignmentID uuid.UUID) ([]QuizAttempt, error) {
+ rows, err := s.pool.Query(ctx, `
+ SELECT
+ id, assignment_id, user_id, answers, score,
+ passed, correct_count, total_count, duration_seconds, attempted_at
+ FROM training_quiz_attempts
+ WHERE assignment_id = $1
+ ORDER BY attempted_at DESC
+ `, assignmentID)
+ if err != nil {
+ return nil, err
+ }
+ defer rows.Close()
+
+ var attempts []QuizAttempt
+ for rows.Next() {
+ var a QuizAttempt
+ var answers []byte
+
+ err := rows.Scan(
+ &a.ID, &a.AssignmentID, &a.UserID, &answers, &a.Score,
+ &a.Passed, &a.CorrectCount, &a.TotalCount, &a.DurationSeconds, &a.AttemptedAt,
+ )
+ if err != nil {
+ return nil, err
+ }
+
+ json.Unmarshal(answers, &a.Answers)
+ if a.Answers == nil {
+ a.Answers = []QuizAnswer{}
+ }
+
+ attempts = append(attempts, a)
+ }
+
+ if attempts == nil {
+ attempts = []QuizAttempt{}
+ }
+
+ return attempts, nil
+}
diff --git a/ai-compliance-sdk/internal/training/store_stats.go b/ai-compliance-sdk/internal/training/store_stats.go
new file mode 100644
index 0000000..a1ddcae
--- /dev/null
+++ b/ai-compliance-sdk/internal/training/store_stats.go
@@ -0,0 +1,120 @@
+package training
+
+import (
+ "context"
+
+ "github.com/google/uuid"
+)
+
+// GetTrainingStats returns aggregated training statistics for a tenant
+func (s *Store) GetTrainingStats(ctx context.Context, tenantID uuid.UUID) (*TrainingStats, error) {
+ stats := &TrainingStats{}
+
+ // Total active modules
+ s.pool.QueryRow(ctx,
+ "SELECT COUNT(*) FROM training_modules WHERE tenant_id = $1 AND is_active = true",
+ tenantID).Scan(&stats.TotalModules)
+
+ // Total assignments
+ s.pool.QueryRow(ctx,
+ "SELECT COUNT(*) FROM training_assignments WHERE tenant_id = $1",
+ tenantID).Scan(&stats.TotalAssignments)
+
+ // Status counts
+ s.pool.QueryRow(ctx,
+ "SELECT COUNT(*) FROM training_assignments WHERE tenant_id = $1 AND status = 'pending'",
+ tenantID).Scan(&stats.PendingCount)
+
+ s.pool.QueryRow(ctx,
+ "SELECT COUNT(*) FROM training_assignments WHERE tenant_id = $1 AND status = 'in_progress'",
+ tenantID).Scan(&stats.InProgressCount)
+
+ s.pool.QueryRow(ctx,
+ "SELECT COUNT(*) FROM training_assignments WHERE tenant_id = $1 AND status = 'completed'",
+ tenantID).Scan(&stats.CompletedCount)
+
+ // Completion rate
+ if stats.TotalAssignments > 0 {
+ stats.CompletionRate = float64(stats.CompletedCount) / float64(stats.TotalAssignments) * 100
+ }
+
+ // Overdue count
+ s.pool.QueryRow(ctx, `
+ SELECT COUNT(*) FROM training_assignments
+ WHERE tenant_id = $1
+ AND status IN ('pending', 'in_progress')
+ AND deadline < NOW()
+ `, tenantID).Scan(&stats.OverdueCount)
+
+ // Average quiz score
+ s.pool.QueryRow(ctx, `
+ SELECT COALESCE(AVG(quiz_score), 0) FROM training_assignments
+ WHERE tenant_id = $1 AND quiz_score IS NOT NULL
+ `, tenantID).Scan(&stats.AvgQuizScore)
+
+ // Average completion days
+ s.pool.QueryRow(ctx, `
+ SELECT COALESCE(AVG(EXTRACT(EPOCH FROM (completed_at - started_at)) / 86400), 0)
+ FROM training_assignments
+ WHERE tenant_id = $1 AND status = 'completed'
+ AND started_at IS NOT NULL AND completed_at IS NOT NULL
+ `, tenantID).Scan(&stats.AvgCompletionDays)
+
+ // Upcoming deadlines (within 7 days)
+ s.pool.QueryRow(ctx, `
+ SELECT COUNT(*) FROM training_assignments
+ WHERE tenant_id = $1
+ AND status IN ('pending', 'in_progress')
+ AND deadline BETWEEN NOW() AND NOW() + INTERVAL '7 days'
+ `, tenantID).Scan(&stats.UpcomingDeadlines)
+
+ return stats, nil
+}
+
+// GetDeadlines returns upcoming deadlines for a tenant
+func (s *Store) GetDeadlines(ctx context.Context, tenantID uuid.UUID, limit int) ([]DeadlineInfo, error) {
+ if limit <= 0 {
+ limit = 20
+ }
+
+ rows, err := s.pool.Query(ctx, `
+ SELECT
+ ta.id, m.module_code, m.title,
+ ta.user_id, ta.user_name, ta.deadline, ta.status,
+ EXTRACT(DAY FROM (ta.deadline - NOW()))::INT AS days_left
+ FROM training_assignments ta
+ JOIN training_modules m ON m.id = ta.module_id
+ WHERE ta.tenant_id = $1
+ AND ta.status IN ('pending', 'in_progress')
+ ORDER BY ta.deadline ASC
+ LIMIT $2
+ `, tenantID, limit)
+ if err != nil {
+ return nil, err
+ }
+ defer rows.Close()
+
+ var deadlines []DeadlineInfo
+ for rows.Next() {
+ var d DeadlineInfo
+ var status string
+
+ err := rows.Scan(
+ &d.AssignmentID, &d.ModuleCode, &d.ModuleTitle,
+ &d.UserID, &d.UserName, &d.Deadline, &status,
+ &d.DaysLeft,
+ )
+ if err != nil {
+ return nil, err
+ }
+
+ d.Status = AssignmentStatus(status)
+ deadlines = append(deadlines, d)
+ }
+
+ if deadlines == nil {
+ deadlines = []DeadlineInfo{}
+ }
+
+ return deadlines, nil
+}
diff --git a/ai-compliance-sdk/internal/ucca/rules.go b/ai-compliance-sdk/internal/ucca/rules.go
index db150c8..e3cffdb 100644
--- a/ai-compliance-sdk/internal/ucca/rules.go
+++ b/ai-compliance-sdk/internal/ucca/rules.go
@@ -285,947 +285,3 @@ func generateAlternative(result *AssessmentResult, intake *UseCaseIntake) string
func (e *RuleEngine) GetRules() []Rule {
return e.rules
}
-
-// ============================================================================
-// Control Definitions
-// ============================================================================
-
-var ControlLibrary = map[string]RequiredControl{
- "C-CONSENT": {
- ID: "C-CONSENT",
- Title: "Einwilligungsmanagement",
- Description: "Implementieren Sie ein System zur Einholung und Verwaltung von Einwilligungen.",
- Severity: SeverityWARN,
- Category: "organizational",
- GDPRRef: "Art. 7 DSGVO",
- },
- "C-PII-DETECT": {
- ID: "C-PII-DETECT",
- Title: "PII-Erkennung",
- Description: "Implementieren Sie automatische Erkennung personenbezogener Daten.",
- Severity: SeverityWARN,
- Category: "technical",
- GDPRRef: "Art. 32 DSGVO",
- },
- "C-ANONYMIZE": {
- ID: "C-ANONYMIZE",
- Title: "Anonymisierung/Pseudonymisierung",
- Description: "Implementieren Sie Anonymisierung oder Pseudonymisierung vor der Verarbeitung.",
- Severity: SeverityWARN,
- Category: "technical",
- GDPRRef: "Art. 32 DSGVO",
- },
- "C-ACCESS-CONTROL": {
- ID: "C-ACCESS-CONTROL",
- Title: "Zugriffskontrollen",
- Description: "Implementieren Sie rollenbasierte Zugriffskontrollen.",
- Severity: SeverityWARN,
- Category: "technical",
- GDPRRef: "Art. 32 DSGVO",
- },
- "C-AUDIT-LOG": {
- ID: "C-AUDIT-LOG",
- Title: "Audit-Logging",
- Description: "Protokollieren Sie alle Zugriffe und Verarbeitungen.",
- Severity: SeverityINFO,
- Category: "technical",
- GDPRRef: "Art. 5(2) DSGVO",
- },
- "C-RETENTION": {
- ID: "C-RETENTION",
- Title: "Aufbewahrungsfristen",
- Description: "Definieren und implementieren Sie automatische Löschfristen.",
- Severity: SeverityWARN,
- Category: "organizational",
- GDPRRef: "Art. 5(1)(e) DSGVO",
- },
- "C-HITL": {
- ID: "C-HITL",
- Title: "Human-in-the-Loop",
- Description: "Implementieren Sie menschliche Überprüfung für KI-Entscheidungen.",
- Severity: SeverityBLOCK,
- Category: "organizational",
- GDPRRef: "Art. 22 DSGVO",
- },
- "C-TRANSPARENCY": {
- ID: "C-TRANSPARENCY",
- Title: "Transparenz",
- Description: "Informieren Sie Betroffene über KI-Verarbeitung.",
- Severity: SeverityWARN,
- Category: "organizational",
- GDPRRef: "Art. 13/14 DSGVO",
- },
- "C-DSR-PROCESS": {
- ID: "C-DSR-PROCESS",
- Title: "Betroffenenrechte-Prozess",
- Description: "Implementieren Sie Prozesse für Auskunft, Löschung, Berichtigung.",
- Severity: SeverityWARN,
- Category: "organizational",
- GDPRRef: "Art. 15-22 DSGVO",
- },
- "C-DSFA": {
- ID: "C-DSFA",
- Title: "DSFA durchführen",
- Description: "Führen Sie eine Datenschutz-Folgenabschätzung durch.",
- Severity: SeverityWARN,
- Category: "organizational",
- GDPRRef: "Art. 35 DSGVO",
- },
- "C-SCC": {
- ID: "C-SCC",
- Title: "Standardvertragsklauseln",
- Description: "Schließen Sie EU-Standardvertragsklauseln für Drittlandtransfers ab.",
- Severity: SeverityBLOCK,
- Category: "legal",
- GDPRRef: "Art. 46 DSGVO",
- },
- "C-ENCRYPTION": {
- ID: "C-ENCRYPTION",
- Title: "Verschlüsselung",
- Description: "Verschlüsseln Sie Daten in Übertragung und Speicherung.",
- Severity: SeverityWARN,
- Category: "technical",
- GDPRRef: "Art. 32 DSGVO",
- },
- "C-MINOR-CONSENT": {
- ID: "C-MINOR-CONSENT",
- Title: "Elterneinwilligung",
- Description: "Holen Sie Einwilligung der Erziehungsberechtigten ein.",
- Severity: SeverityBLOCK,
- Category: "organizational",
- GDPRRef: "Art. 8 DSGVO",
- },
- "C-ART9-BASIS": {
- ID: "C-ART9-BASIS",
- Title: "Art. 9 Rechtsgrundlage",
- Description: "Dokumentieren Sie die Rechtsgrundlage für besondere Datenkategorien.",
- Severity: SeverityBLOCK,
- Category: "legal",
- GDPRRef: "Art. 9 DSGVO",
- },
-}
-
-// GetControlByID returns a control by its ID
-func GetControlByID(id string) *RequiredControl {
- if ctrl, exists := ControlLibrary[id]; exists {
- return &ctrl
- }
- return nil
-}
-
-// ============================================================================
-// All Rules (~45 rules in 10 categories)
-// ============================================================================
-
-var AllRules = []Rule{
- // =========================================================================
- // A. Datenklassifikation (R-001 bis R-006)
- // =========================================================================
- {
- Code: "R-001",
- Category: "A. Datenklassifikation",
- Title: "Personal Data Processing",
- TitleDE: "Verarbeitung personenbezogener Daten",
- Description: "Personal data is being processed",
- DescriptionDE: "Personenbezogene Daten werden verarbeitet",
- Severity: SeverityINFO,
- ScoreDelta: 5,
- GDPRRef: "Art. 4(1) DSGVO",
- Controls: []string{"C-PII-DETECT", "C-ACCESS-CONTROL"},
- Patterns: []string{"P-PRE-ANON"},
- Condition: func(intake *UseCaseIntake) bool {
- return intake.DataTypes.PersonalData
- },
- Rationale: func(intake *UseCaseIntake) string {
- return "Der Use Case verarbeitet personenbezogene Daten. Dies erfordert eine Rechtsgrundlage und entsprechende Schutzmaßnahmen."
- },
- },
- {
- Code: "R-002",
- Category: "A. Datenklassifikation",
- Title: "Special Category Data (Art. 9)",
- TitleDE: "Besondere Kategorien personenbezogener Daten (Art. 9)",
- Description: "Processing of special category data requires explicit consent or legal basis",
- DescriptionDE: "Verarbeitung besonderer Datenkategorien erfordert ausdrückliche Einwilligung oder Rechtsgrundlage",
- Severity: SeverityWARN,
- ScoreDelta: 20,
- GDPRRef: "Art. 9 DSGVO",
- Controls: []string{"C-ART9-BASIS", "C-DSFA", "C-ENCRYPTION"},
- Patterns: []string{"P-PRE-ANON", "P-HITL-ENFORCED"},
- Condition: func(intake *UseCaseIntake) bool {
- return intake.DataTypes.Article9Data
- },
- Rationale: func(intake *UseCaseIntake) string {
- return "Besondere Kategorien personenbezogener Daten (Gesundheit, Religion, etc.) erfordern besondere Schutzmaßnahmen und eine spezifische Rechtsgrundlage nach Art. 9 DSGVO."
- },
- },
- {
- Code: "R-003",
- Category: "A. Datenklassifikation",
- Title: "Minor Data Processing",
- TitleDE: "Verarbeitung von Daten Minderjähriger",
- Description: "Processing data of children requires special protections",
- DescriptionDE: "Verarbeitung von Daten Minderjähriger erfordert besonderen Schutz",
- Severity: SeverityWARN,
- ScoreDelta: 15,
- GDPRRef: "Art. 8 DSGVO",
- Controls: []string{"C-MINOR-CONSENT", "C-DSFA"},
- Patterns: []string{"P-HITL-ENFORCED"},
- Condition: func(intake *UseCaseIntake) bool {
- return intake.DataTypes.MinorData
- },
- Rationale: func(intake *UseCaseIntake) string {
- return "Daten von Minderjährigen erfordern besonderen Schutz. Die Einwilligung muss von Erziehungsberechtigten eingeholt werden."
- },
- },
- {
- Code: "R-004",
- Category: "A. Datenklassifikation",
- Title: "Biometric Data",
- TitleDE: "Biometrische Daten",
- Description: "Biometric data processing is high risk",
- DescriptionDE: "Verarbeitung biometrischer Daten ist hochriskant",
- Severity: SeverityWARN,
- ScoreDelta: 20,
- GDPRRef: "Art. 9 DSGVO",
- Controls: []string{"C-ART9-BASIS", "C-DSFA", "C-ENCRYPTION"},
- Patterns: []string{"P-PRE-ANON"},
- Condition: func(intake *UseCaseIntake) bool {
- return intake.DataTypes.BiometricData
- },
- Rationale: func(intake *UseCaseIntake) string {
- return "Biometrische Daten zur eindeutigen Identifizierung fallen unter Art. 9 DSGVO und erfordern eine DSFA."
- },
- },
- {
- Code: "R-005",
- Category: "A. Datenklassifikation",
- Title: "Location Data",
- TitleDE: "Standortdaten",
- Description: "Location tracking requires transparency and consent",
- DescriptionDE: "Standortverfolgung erfordert Transparenz und Einwilligung",
- Severity: SeverityINFO,
- ScoreDelta: 10,
- GDPRRef: "Art. 5, Art. 7 DSGVO",
- Controls: []string{"C-CONSENT", "C-TRANSPARENCY"},
- Patterns: []string{"P-LOG-MINIMIZATION"},
- Condition: func(intake *UseCaseIntake) bool {
- return intake.DataTypes.LocationData
- },
- Rationale: func(intake *UseCaseIntake) string {
- return "Standortdaten ermöglichen Bewegungsprofile und erfordern klare Einwilligung und Aufbewahrungslimits."
- },
- },
- {
- Code: "R-006",
- Category: "A. Datenklassifikation",
- Title: "Employee Data",
- TitleDE: "Mitarbeiterdaten",
- Description: "Employee data processing has special considerations",
- DescriptionDE: "Mitarbeiterdatenverarbeitung hat besondere Anforderungen",
- Severity: SeverityINFO,
- ScoreDelta: 10,
- GDPRRef: "§ 26 BDSG",
- Controls: []string{"C-ACCESS-CONTROL", "C-TRANSPARENCY"},
- Patterns: []string{"P-NAMESPACE-ISOLATION"},
- Condition: func(intake *UseCaseIntake) bool {
- return intake.DataTypes.EmployeeData
- },
- Rationale: func(intake *UseCaseIntake) string {
- return "Mitarbeiterdaten unterliegen zusätzlich dem BDSG § 26 und erfordern klare Zweckbindung."
- },
- },
- // =========================================================================
- // B. Zweck & Kontext (R-010 bis R-013)
- // =========================================================================
- {
- Code: "R-010",
- Category: "B. Zweck & Kontext",
- Title: "Marketing with Personal Data",
- TitleDE: "Marketing mit personenbezogenen Daten",
- Description: "Marketing purposes with PII require explicit consent",
- DescriptionDE: "Marketing mit PII erfordert ausdrückliche Einwilligung",
- Severity: SeverityWARN,
- ScoreDelta: 15,
- GDPRRef: "Art. 6(1)(a) DSGVO",
- Controls: []string{"C-CONSENT", "C-DSR-PROCESS"},
- Patterns: []string{"P-PRE-ANON"},
- Condition: func(intake *UseCaseIntake) bool {
- return intake.Purpose.Marketing && intake.DataTypes.PersonalData
- },
- Rationale: func(intake *UseCaseIntake) string {
- return "Marketing mit personenbezogenen Daten erfordert ausdrückliche, freiwillige Einwilligung."
- },
- },
- {
- Code: "R-011",
- Category: "B. Zweck & Kontext",
- Title: "Profiling Purpose",
- TitleDE: "Profiling-Zweck",
- Description: "Profiling requires DSFA and transparency",
- DescriptionDE: "Profiling erfordert DSFA und Transparenz",
- Severity: SeverityWARN,
- ScoreDelta: 15,
- GDPRRef: "Art. 22 DSGVO",
- Controls: []string{"C-DSFA", "C-TRANSPARENCY", "C-DSR-PROCESS"},
- Patterns: []string{"P-HITL-ENFORCED"},
- Condition: func(intake *UseCaseIntake) bool {
- return intake.Purpose.Profiling
- },
- Rationale: func(intake *UseCaseIntake) string {
- return "Profiling erfordert eine DSFA und transparente Information der Betroffenen über die Logik und Auswirkungen."
- },
- },
- {
- Code: "R-012",
- Category: "B. Zweck & Kontext",
- Title: "Evaluation/Scoring Purpose",
- TitleDE: "Bewertungs-/Scoring-Zweck",
- Description: "Scoring of individuals requires safeguards",
- DescriptionDE: "Scoring von Personen erfordert Schutzmaßnahmen",
- Severity: SeverityWARN,
- ScoreDelta: 15,
- GDPRRef: "Art. 22 DSGVO",
- Controls: []string{"C-HITL", "C-TRANSPARENCY"},
- Patterns: []string{"P-HITL-ENFORCED"},
- Condition: func(intake *UseCaseIntake) bool {
- return intake.Purpose.EvaluationScoring
- },
- Rationale: func(intake *UseCaseIntake) string {
- return "Bewertung/Scoring von Personen erfordert menschliche Überprüfung und Transparenz über die verwendete Logik."
- },
- },
- {
- Code: "R-013",
- Category: "B. Zweck & Kontext",
- Title: "Customer Support - Low Risk",
- TitleDE: "Kundenservice - Niedriges Risiko",
- Description: "Customer support without PII storage is low risk",
- DescriptionDE: "Kundenservice ohne PII-Speicherung ist risikoarm",
- Severity: SeverityINFO,
- ScoreDelta: 0,
- GDPRRef: "",
- Controls: []string{},
- Patterns: []string{"P-RAG-ONLY"},
- Condition: func(intake *UseCaseIntake) bool {
- return intake.Purpose.CustomerSupport && !intake.DataTypes.PersonalData
- },
- Rationale: func(intake *UseCaseIntake) string {
- return "Kundenservice mit öffentlichen FAQ-Daten ohne Speicherung personenbezogener Daten ist risikoarm."
- },
- },
- // =========================================================================
- // C. Automatisierung (R-020 bis R-025)
- // =========================================================================
- {
- Code: "R-020",
- Category: "C. Automatisierung",
- Title: "Fully Automated with Legal Effects",
- TitleDE: "Vollautomatisiert mit rechtlichen Auswirkungen",
- Description: "Fully automated decisions with legal effects violate Art. 22",
- DescriptionDE: "Vollautomatisierte Entscheidungen mit rechtlichen Auswirkungen verletzen Art. 22",
- Severity: SeverityBLOCK,
- ScoreDelta: 40,
- GDPRRef: "Art. 22 DSGVO",
- Controls: []string{"C-HITL"},
- Patterns: []string{"P-HITL-ENFORCED"},
- Condition: func(intake *UseCaseIntake) bool {
- return intake.Automation == AutomationFullyAutomated && intake.Outputs.LegalEffects
- },
- Rationale: func(intake *UseCaseIntake) string {
- return "Vollautomatisierte Entscheidungen mit rechtlichen Auswirkungen ohne menschliche Beteiligung sind nach Art. 22 DSGVO unzulässig."
- },
- },
- {
- Code: "R-021",
- Category: "C. Automatisierung",
- Title: "Fully Automated Rankings/Scores",
- TitleDE: "Vollautomatisierte Rankings/Scores",
- Description: "Automated scoring requires human review",
- DescriptionDE: "Automatisches Scoring erfordert menschliche Überprüfung",
- Severity: SeverityWARN,
- ScoreDelta: 20,
- GDPRRef: "Art. 22 DSGVO",
- Controls: []string{"C-HITL", "C-TRANSPARENCY"},
- Patterns: []string{"P-HITL-ENFORCED"},
- Condition: func(intake *UseCaseIntake) bool {
- return intake.Automation == AutomationFullyAutomated && intake.Outputs.RankingsOrScores
- },
- Rationale: func(intake *UseCaseIntake) string {
- return "Vollautomatisierte Erstellung von Rankings oder Scores erfordert menschliche Überprüfung vor Verwendung."
- },
- },
- {
- Code: "R-022",
- Category: "C. Automatisierung",
- Title: "Fully Automated Access Decisions",
- TitleDE: "Vollautomatisierte Zugriffsentscheidungen",
- Description: "Automated access decisions need safeguards",
- DescriptionDE: "Automatisierte Zugriffsentscheidungen benötigen Schutzmaßnahmen",
- Severity: SeverityWARN,
- ScoreDelta: 15,
- GDPRRef: "Art. 22 DSGVO",
- Controls: []string{"C-HITL", "C-TRANSPARENCY", "C-DSR-PROCESS"},
- Patterns: []string{"P-HITL-ENFORCED"},
- Condition: func(intake *UseCaseIntake) bool {
- return intake.Automation == AutomationFullyAutomated && intake.Outputs.AccessDecisions
- },
- Rationale: func(intake *UseCaseIntake) string {
- return "Automatisierte Entscheidungen über Zugang erfordern Widerspruchsmöglichkeit und menschliche Überprüfung."
- },
- },
- {
- Code: "R-023",
- Category: "C. Automatisierung",
- Title: "Semi-Automated - Medium Risk",
- TitleDE: "Teilautomatisiert - Mittleres Risiko",
- Description: "Semi-automated processing with human review",
- DescriptionDE: "Teilautomatisierte Verarbeitung mit menschlicher Überprüfung",
- Severity: SeverityINFO,
- ScoreDelta: 5,
- GDPRRef: "",
- Controls: []string{"C-AUDIT-LOG"},
- Patterns: []string{"P-HITL-ENFORCED"},
- Condition: func(intake *UseCaseIntake) bool {
- return intake.Automation == AutomationSemiAutomated && intake.DataTypes.PersonalData
- },
- Rationale: func(intake *UseCaseIntake) string {
- return "Teilautomatisierte Verarbeitung mit menschlicher Überprüfung ist grundsätzlich konform, erfordert aber Dokumentation."
- },
- },
- {
- Code: "R-024",
- Category: "C. Automatisierung",
- Title: "Assistive Only - Low Risk",
- TitleDE: "Nur assistierend - Niedriges Risiko",
- Description: "Assistive AI without automated decisions is low risk",
- DescriptionDE: "Assistive KI ohne automatisierte Entscheidungen ist risikoarm",
- Severity: SeverityINFO,
- ScoreDelta: 0,
- GDPRRef: "",
- Controls: []string{},
- Patterns: []string{"P-RAG-ONLY"},
- Condition: func(intake *UseCaseIntake) bool {
- return intake.Automation == AutomationAssistive
- },
- Rationale: func(intake *UseCaseIntake) string {
- return "Rein assistive KI, die nur Vorschläge macht und keine Entscheidungen trifft, ist risikoarm."
- },
- },
- {
- Code: "R-025",
- Category: "C. Automatisierung",
- Title: "HR Scoring - Blocked",
- TitleDE: "HR-Scoring - Blockiert",
- Description: "Automated HR scoring/evaluation is prohibited",
- DescriptionDE: "Automatisiertes HR-Scoring/Bewertung ist verboten",
- Severity: SeverityBLOCK,
- ScoreDelta: 50,
- GDPRRef: "Art. 22, § 26 BDSG",
- Controls: []string{"C-HITL"},
- Patterns: []string{},
- Condition: func(intake *UseCaseIntake) bool {
- return intake.Domain == DomainHR &&
- intake.Purpose.EvaluationScoring &&
- intake.Automation == AutomationFullyAutomated
- },
- Rationale: func(intake *UseCaseIntake) string {
- return "Vollautomatisierte Bewertung/Scoring von Mitarbeitern ist unzulässig. Arbeitsrechtliche Entscheidungen müssen von Menschen getroffen werden."
- },
- },
- // =========================================================================
- // D. Training vs Nutzung (R-030 bis R-035)
- // =========================================================================
- {
- Code: "R-030",
- Category: "D. Training vs Nutzung",
- Title: "Training with Personal Data",
- TitleDE: "Training mit personenbezogenen Daten",
- Description: "Training AI with personal data is high risk",
- DescriptionDE: "Training von KI mit personenbezogenen Daten ist hochriskant",
- Severity: SeverityBLOCK,
- ScoreDelta: 40,
- GDPRRef: "Art. 5(1)(b)(c) DSGVO",
- Controls: []string{"C-ART9-BASIS", "C-DSFA"},
- Patterns: []string{"P-RAG-ONLY"},
- Condition: func(intake *UseCaseIntake) bool {
- return intake.ModelUsage.Training && intake.DataTypes.PersonalData
- },
- Rationale: func(intake *UseCaseIntake) string {
- return "Training von KI-Modellen mit personenbezogenen Daten verstößt gegen Zweckbindung und Datenminimierung. Nutzen Sie stattdessen RAG."
- },
- },
- {
- Code: "R-031",
- Category: "D. Training vs Nutzung",
- Title: "Fine-tuning with Personal Data",
- TitleDE: "Fine-Tuning mit personenbezogenen Daten",
- Description: "Fine-tuning with PII requires safeguards",
- DescriptionDE: "Fine-Tuning mit PII erfordert Schutzmaßnahmen",
- Severity: SeverityWARN,
- ScoreDelta: 25,
- GDPRRef: "Art. 5(1)(b)(c) DSGVO",
- Controls: []string{"C-ANONYMIZE", "C-DSFA"},
- Patterns: []string{"P-PRE-ANON"},
- Condition: func(intake *UseCaseIntake) bool {
- return intake.ModelUsage.Finetune && intake.DataTypes.PersonalData
- },
- Rationale: func(intake *UseCaseIntake) string {
- return "Fine-Tuning mit personenbezogenen Daten ist nur nach Anonymisierung/Pseudonymisierung zulässig."
- },
- },
- {
- Code: "R-032",
- Category: "D. Training vs Nutzung",
- Title: "RAG Only - Recommended",
- TitleDE: "Nur RAG - Empfohlen",
- Description: "RAG without training is the safest approach",
- DescriptionDE: "RAG ohne Training ist der sicherste Ansatz",
- Severity: SeverityINFO,
- ScoreDelta: 0,
- GDPRRef: "",
- Controls: []string{},
- Patterns: []string{"P-RAG-ONLY"},
- Condition: func(intake *UseCaseIntake) bool {
- return intake.ModelUsage.RAG && !intake.ModelUsage.Training && !intake.ModelUsage.Finetune
- },
- Rationale: func(intake *UseCaseIntake) string {
- return "Nur-RAG ohne Training oder Fine-Tuning ist die empfohlene Architektur für DSGVO-Konformität."
- },
- },
- {
- Code: "R-033",
- Category: "D. Training vs Nutzung",
- Title: "Training with Article 9 Data",
- TitleDE: "Training mit Art. 9 Daten",
- Description: "Training with special category data is prohibited",
- DescriptionDE: "Training mit besonderen Datenkategorien ist verboten",
- Severity: SeverityBLOCK,
- ScoreDelta: 50,
- GDPRRef: "Art. 9 DSGVO",
- Controls: []string{},
- Patterns: []string{},
- Condition: func(intake *UseCaseIntake) bool {
- return (intake.ModelUsage.Training || intake.ModelUsage.Finetune) && intake.DataTypes.Article9Data
- },
- Rationale: func(intake *UseCaseIntake) string {
- return "Training oder Fine-Tuning mit besonderen Kategorien personenbezogener Daten (Gesundheit, Religion, etc.) ist grundsätzlich unzulässig."
- },
- },
- {
- Code: "R-034",
- Category: "D. Training vs Nutzung",
- Title: "Inference with Public Data",
- TitleDE: "Inferenz mit öffentlichen Daten",
- Description: "Using only public data is low risk",
- DescriptionDE: "Nutzung nur öffentlicher Daten ist risikoarm",
- Severity: SeverityINFO,
- ScoreDelta: 0,
- GDPRRef: "",
- Controls: []string{},
- Patterns: []string{},
- Condition: func(intake *UseCaseIntake) bool {
- return intake.DataTypes.PublicData && !intake.DataTypes.PersonalData
- },
- Rationale: func(intake *UseCaseIntake) string {
- return "Die ausschließliche Nutzung öffentlich zugänglicher Daten ohne Personenbezug ist unproblematisch."
- },
- },
- {
- Code: "R-035",
- Category: "D. Training vs Nutzung",
- Title: "Training with Minor Data",
- TitleDE: "Training mit Daten Minderjähriger",
- Description: "Training with children's data is prohibited",
- DescriptionDE: "Training mit Kinderdaten ist verboten",
- Severity: SeverityBLOCK,
- ScoreDelta: 50,
- GDPRRef: "Art. 8 DSGVO, ErwG 38",
- Controls: []string{},
- Patterns: []string{},
- Condition: func(intake *UseCaseIntake) bool {
- return (intake.ModelUsage.Training || intake.ModelUsage.Finetune) && intake.DataTypes.MinorData
- },
- Rationale: func(intake *UseCaseIntake) string {
- return "Training von KI-Modellen mit Daten von Minderjährigen ist aufgrund des besonderen Schutzes unzulässig."
- },
- },
- // =========================================================================
- // E. Speicherung (R-040 bis R-042)
- // =========================================================================
- {
- Code: "R-040",
- Category: "E. Speicherung",
- Title: "Storing Prompts with PII",
- TitleDE: "Speicherung von Prompts mit PII",
- Description: "Storing prompts containing PII requires controls",
- DescriptionDE: "Speicherung von Prompts mit PII erfordert Kontrollen",
- Severity: SeverityWARN,
- ScoreDelta: 15,
- GDPRRef: "Art. 5(1)(e) DSGVO",
- Controls: []string{"C-RETENTION", "C-ANONYMIZE", "C-DSR-PROCESS"},
- Patterns: []string{"P-LOG-MINIMIZATION", "P-PRE-ANON"},
- Condition: func(intake *UseCaseIntake) bool {
- return intake.Retention.StorePrompts && intake.DataTypes.PersonalData
- },
- Rationale: func(intake *UseCaseIntake) string {
- return "Speicherung von Prompts mit personenbezogenen Daten erfordert Löschfristen und Anonymisierungsoptionen."
- },
- },
- {
- Code: "R-041",
- Category: "E. Speicherung",
- Title: "Storing Responses with PII",
- TitleDE: "Speicherung von Antworten mit PII",
- Description: "Storing AI responses containing PII requires controls",
- DescriptionDE: "Speicherung von KI-Antworten mit PII erfordert Kontrollen",
- Severity: SeverityWARN,
- ScoreDelta: 10,
- GDPRRef: "Art. 5(1)(e) DSGVO",
- Controls: []string{"C-RETENTION", "C-DSR-PROCESS"},
- Patterns: []string{"P-LOG-MINIMIZATION"},
- Condition: func(intake *UseCaseIntake) bool {
- return intake.Retention.StoreResponses && intake.DataTypes.PersonalData
- },
- Rationale: func(intake *UseCaseIntake) string {
- return "Speicherung von KI-Antworten mit personenbezogenen Daten erfordert definierte Aufbewahrungsfristen."
- },
- },
- {
- Code: "R-042",
- Category: "E. Speicherung",
- Title: "No Retention Policy",
- TitleDE: "Keine Aufbewahrungsrichtlinie",
- Description: "PII storage without retention limits is problematic",
- DescriptionDE: "PII-Speicherung ohne Aufbewahrungslimits ist problematisch",
- Severity: SeverityWARN,
- ScoreDelta: 10,
- GDPRRef: "Art. 5(1)(e) DSGVO",
- Controls: []string{"C-RETENTION"},
- Patterns: []string{"P-LOG-MINIMIZATION"},
- Condition: func(intake *UseCaseIntake) bool {
- return (intake.Retention.StorePrompts || intake.Retention.StoreResponses) &&
- intake.DataTypes.PersonalData &&
- intake.Retention.RetentionDays == 0
- },
- Rationale: func(intake *UseCaseIntake) string {
- return "Speicherung personenbezogener Daten ohne definierte Aufbewahrungsfrist verstößt gegen den Grundsatz der Speicherbegrenzung."
- },
- },
- // =========================================================================
- // F. Hosting (R-050 bis R-052)
- // =========================================================================
- {
- Code: "R-050",
- Category: "F. Hosting",
- Title: "Third Country Transfer with PII",
- TitleDE: "Drittlandtransfer mit PII",
- Description: "Transferring PII to third countries requires safeguards",
- DescriptionDE: "Übermittlung von PII in Drittländer erfordert Schutzmaßnahmen",
- Severity: SeverityWARN,
- ScoreDelta: 20,
- GDPRRef: "Art. 44-49 DSGVO",
- Controls: []string{"C-SCC", "C-ENCRYPTION"},
- Patterns: []string{},
- Condition: func(intake *UseCaseIntake) bool {
- return intake.Hosting.Region == "third_country" && intake.DataTypes.PersonalData
- },
- Rationale: func(intake *UseCaseIntake) string {
- return "Übermittlung personenbezogener Daten in Drittländer erfordert Standardvertragsklauseln oder andere geeignete Garantien."
- },
- },
- {
- Code: "R-051",
- Category: "F. Hosting",
- Title: "EU Hosting - Compliant",
- TitleDE: "EU-Hosting - Konform",
- Description: "Hosting within EU is compliant with GDPR",
- DescriptionDE: "Hosting innerhalb der EU ist DSGVO-konform",
- Severity: SeverityINFO,
- ScoreDelta: 0,
- GDPRRef: "",
- Controls: []string{},
- Patterns: []string{},
- Condition: func(intake *UseCaseIntake) bool {
- return intake.Hosting.Region == "eu"
- },
- Rationale: func(intake *UseCaseIntake) string {
- return "Hosting innerhalb der EU/EWR erfüllt grundsätzlich die DSGVO-Anforderungen an den Datenstandort."
- },
- },
- {
- Code: "R-052",
- Category: "F. Hosting",
- Title: "On-Premise Hosting",
- TitleDE: "On-Premise-Hosting",
- Description: "On-premise hosting gives most control",
- DescriptionDE: "On-Premise-Hosting gibt die meiste Kontrolle",
- Severity: SeverityINFO,
- ScoreDelta: 0,
- GDPRRef: "",
- Controls: []string{"C-ENCRYPTION"},
- Patterns: []string{},
- Condition: func(intake *UseCaseIntake) bool {
- return intake.Hosting.Region == "on_prem"
- },
- Rationale: func(intake *UseCaseIntake) string {
- return "On-Premise-Hosting bietet maximale Kontrolle über Daten, erfordert aber eigene Sicherheitsmaßnahmen."
- },
- },
- // =========================================================================
- // G. Transparenz (R-060 bis R-062)
- // =========================================================================
- {
- Code: "R-060",
- Category: "G. Transparenz",
- Title: "No Human Review for Decisions",
- TitleDE: "Keine menschliche Überprüfung bei Entscheidungen",
- Description: "Decisions affecting individuals need human review option",
- DescriptionDE: "Entscheidungen, die Personen betreffen, benötigen menschliche Überprüfungsoption",
- Severity: SeverityWARN,
- ScoreDelta: 15,
- GDPRRef: "Art. 22(3) DSGVO",
- Controls: []string{"C-HITL", "C-DSR-PROCESS"},
- Patterns: []string{"P-HITL-ENFORCED"},
- Condition: func(intake *UseCaseIntake) bool {
- return (intake.Outputs.LegalEffects || intake.Outputs.AccessDecisions || intake.Purpose.DecisionMaking) &&
- intake.Automation != AutomationAssistive
- },
- Rationale: func(intake *UseCaseIntake) string {
- return "Betroffene haben das Recht auf menschliche Überprüfung bei automatisierten Entscheidungen."
- },
- },
- {
- Code: "R-061",
- Category: "G. Transparenz",
- Title: "External Recommendations",
- TitleDE: "Externe Empfehlungen",
- Description: "Recommendations to users need transparency",
- DescriptionDE: "Empfehlungen an Nutzer erfordern Transparenz",
- Severity: SeverityINFO,
- ScoreDelta: 5,
- GDPRRef: "Art. 13/14 DSGVO",
- Controls: []string{"C-TRANSPARENCY"},
- Patterns: []string{},
- Condition: func(intake *UseCaseIntake) bool {
- return intake.Outputs.RecommendationsToUsers && intake.DataTypes.PersonalData
- },
- Rationale: func(intake *UseCaseIntake) string {
- return "Personalisierte Empfehlungen erfordern Information der Nutzer über die KI-Verarbeitung."
- },
- },
- {
- Code: "R-062",
- Category: "G. Transparenz",
- Title: "Content Generation without Disclosure",
- TitleDE: "Inhaltsgenerierung ohne Offenlegung",
- Description: "AI-generated content should be disclosed",
- DescriptionDE: "KI-generierte Inhalte sollten offengelegt werden",
- Severity: SeverityINFO,
- ScoreDelta: 5,
- GDPRRef: "EU-AI-Act Art. 52",
- Controls: []string{"C-TRANSPARENCY"},
- Patterns: []string{},
- Condition: func(intake *UseCaseIntake) bool {
- return intake.Outputs.ContentGeneration
- },
- Rationale: func(intake *UseCaseIntake) string {
- return "KI-generierte Inhalte sollten als solche gekennzeichnet werden (EU-AI-Act Transparenzpflicht)."
- },
- },
- // =========================================================================
- // H. Domain-spezifisch (R-070 bis R-074)
- // =========================================================================
- {
- Code: "R-070",
- Category: "H. Domain-spezifisch",
- Title: "Education + Scoring = Blocked",
- TitleDE: "Bildung + Scoring = Blockiert",
- Description: "Automated scoring of students is prohibited",
- DescriptionDE: "Automatisches Scoring von Schülern ist verboten",
- Severity: SeverityBLOCK,
- ScoreDelta: 50,
- GDPRRef: "Art. 8, Art. 22 DSGVO",
- Controls: []string{},
- Patterns: []string{},
- Condition: func(intake *UseCaseIntake) bool {
- return intake.Domain == DomainEducation &&
- intake.DataTypes.MinorData &&
- (intake.Purpose.EvaluationScoring || intake.Outputs.RankingsOrScores)
- },
- Rationale: func(intake *UseCaseIntake) string {
- return "Automatisches Scoring oder Ranking von Schülern/Minderjährigen ist aufgrund des besonderen Schutzes unzulässig."
- },
- },
- {
- Code: "R-071",
- Category: "H. Domain-spezifisch",
- Title: "Healthcare + Automated Diagnosis",
- TitleDE: "Gesundheit + Automatische Diagnose",
- Description: "Automated medical decisions require strict controls",
- DescriptionDE: "Automatische medizinische Entscheidungen erfordern strenge Kontrollen",
- Severity: SeverityBLOCK,
- ScoreDelta: 45,
- GDPRRef: "Art. 9, Art. 22 DSGVO",
- Controls: []string{"C-HITL", "C-DSFA", "C-ART9-BASIS"},
- Patterns: []string{"P-HITL-ENFORCED"},
- Condition: func(intake *UseCaseIntake) bool {
- return intake.Domain == DomainHealthcare &&
- intake.Automation == AutomationFullyAutomated &&
- intake.Purpose.DecisionMaking
- },
- Rationale: func(intake *UseCaseIntake) string {
- return "Vollautomatisierte medizinische Diagnosen oder Behandlungsentscheidungen sind ohne ärztliche Überprüfung unzulässig."
- },
- },
- {
- Code: "R-072",
- Category: "H. Domain-spezifisch",
- Title: "Finance + Automated Credit Scoring",
- TitleDE: "Finanzen + Automatisches Credit-Scoring",
- Description: "Automated credit decisions require transparency",
- DescriptionDE: "Automatische Kreditentscheidungen erfordern Transparenz",
- Severity: SeverityWARN,
- ScoreDelta: 20,
- GDPRRef: "Art. 22 DSGVO",
- Controls: []string{"C-HITL", "C-TRANSPARENCY", "C-DSR-PROCESS"},
- Patterns: []string{"P-HITL-ENFORCED"},
- Condition: func(intake *UseCaseIntake) bool {
- return intake.Domain == DomainFinance &&
- (intake.Purpose.EvaluationScoring || intake.Outputs.RankingsOrScores) &&
- intake.DataTypes.FinancialData
- },
- Rationale: func(intake *UseCaseIntake) string {
- return "Automatische Kreditwürdigkeitsprüfung erfordert Erklärbarkeit und Widerspruchsmöglichkeit."
- },
- },
- {
- Code: "R-073",
- Category: "H. Domain-spezifisch",
- Title: "Utilities + RAG Chatbot = Low Risk",
- TitleDE: "Versorgungsunternehmen + RAG-Chatbot = Niedriges Risiko",
- Description: "RAG-based customer service chatbot is low risk",
- DescriptionDE: "RAG-basierter Kundenservice-Chatbot ist risikoarm",
- Severity: SeverityINFO,
- ScoreDelta: 0,
- GDPRRef: "",
- Controls: []string{},
- Patterns: []string{"P-RAG-ONLY"},
- Condition: func(intake *UseCaseIntake) bool {
- return intake.Domain == DomainUtilities &&
- intake.ModelUsage.RAG &&
- intake.Purpose.CustomerSupport &&
- !intake.DataTypes.PersonalData
- },
- Rationale: func(intake *UseCaseIntake) string {
- return "Ein RAG-basierter Kundenservice-Chatbot ohne Speicherung personenbezogener Daten ist ein Best-Practice-Beispiel."
- },
- },
- {
- Code: "R-074",
- Category: "H. Domain-spezifisch",
- Title: "Public Sector + Automated Decisions",
- TitleDE: "Öffentlicher Sektor + Automatische Entscheidungen",
- Description: "Public sector automated decisions need special care",
- DescriptionDE: "Automatische Entscheidungen im öffentlichen Sektor erfordern besondere Sorgfalt",
- Severity: SeverityWARN,
- ScoreDelta: 20,
- GDPRRef: "Art. 22 DSGVO",
- Controls: []string{"C-HITL", "C-TRANSPARENCY", "C-DSFA"},
- Patterns: []string{"P-HITL-ENFORCED"},
- Condition: func(intake *UseCaseIntake) bool {
- return intake.Domain == DomainPublic &&
- intake.Purpose.DecisionMaking &&
- intake.Automation != AutomationAssistive
- },
- Rationale: func(intake *UseCaseIntake) string {
- return "Verwaltungsentscheidungen, die Bürger betreffen, erfordern besondere Transparenz und Überprüfungsmöglichkeiten."
- },
- },
- // =========================================================================
- // I. Aggregation (R-090 bis R-092) - Implicit in Evaluate()
- // =========================================================================
- {
- Code: "R-090",
- Category: "I. Aggregation",
- Title: "Block Rules Triggered",
- TitleDE: "Blockierungsregeln ausgelöst",
- Description: "Any BLOCK severity results in NO feasibility",
- DescriptionDE: "Jede BLOCK-Schwere führt zu NEIN-Machbarkeit",
- Severity: SeverityBLOCK,
- ScoreDelta: 0,
- GDPRRef: "",
- Controls: []string{},
- Patterns: []string{},
- Condition: func(intake *UseCaseIntake) bool {
- // This is handled in aggregation logic
- return false
- },
- Rationale: func(intake *UseCaseIntake) string {
- return "Eine oder mehrere kritische Regelverletzungen führen zur Einstufung als nicht umsetzbar."
- },
- },
- {
- Code: "R-091",
- Category: "I. Aggregation",
- Title: "Warning Rules Only",
- TitleDE: "Nur Warnungsregeln",
- Description: "Only WARN severity results in CONDITIONAL",
- DescriptionDE: "Nur WARN-Schwere führt zu BEDINGT",
- Severity: SeverityWARN,
- ScoreDelta: 0,
- GDPRRef: "",
- Controls: []string{},
- Patterns: []string{},
- Condition: func(intake *UseCaseIntake) bool {
- // This is handled in aggregation logic
- return false
- },
- Rationale: func(intake *UseCaseIntake) string {
- return "Warnungen erfordern Maßnahmen, blockieren aber nicht die Umsetzung."
- },
- },
- {
- Code: "R-092",
- Category: "I. Aggregation",
- Title: "Info Only - Clear Path",
- TitleDE: "Nur Info - Freier Weg",
- Description: "Only INFO severity results in YES",
- DescriptionDE: "Nur INFO-Schwere führt zu JA",
- Severity: SeverityINFO,
- ScoreDelta: 0,
- GDPRRef: "",
- Controls: []string{},
- Patterns: []string{},
- Condition: func(intake *UseCaseIntake) bool {
- // This is handled in aggregation logic
- return false
- },
- Rationale: func(intake *UseCaseIntake) string {
- return "Keine kritischen oder warnenden Regeln ausgelöst - Umsetzung empfohlen."
- },
- },
- // =========================================================================
- // J. Erklärung (R-100)
- // =========================================================================
- {
- Code: "R-100",
- Category: "J. Erklärung",
- Title: "Rejection Must Include Reason and Alternative",
- TitleDE: "Ablehnung muss Begründung und Alternative enthalten",
- Description: "When feasibility is NO, provide reason and alternative",
- DescriptionDE: "Bei Machbarkeit NEIN, Begründung und Alternative angeben",
- Severity: SeverityINFO,
- ScoreDelta: 0,
- GDPRRef: "",
- Controls: []string{},
- Patterns: []string{},
- Condition: func(intake *UseCaseIntake) bool {
- // This is handled in summary generation
- return false
- },
- Rationale: func(intake *UseCaseIntake) string {
- return "Jede Ablehnung enthält eine klare Begründung und einen alternativen Ansatz."
- },
- },
-}
diff --git a/ai-compliance-sdk/internal/ucca/rules_controls.go b/ai-compliance-sdk/internal/ucca/rules_controls.go
new file mode 100644
index 0000000..e304294
--- /dev/null
+++ b/ai-compliance-sdk/internal/ucca/rules_controls.go
@@ -0,0 +1,128 @@
+package ucca
+
+// ============================================================================
+// Control Definitions
+// ============================================================================
+
+var ControlLibrary = map[string]RequiredControl{
+ "C-CONSENT": {
+ ID: "C-CONSENT",
+ Title: "Einwilligungsmanagement",
+ Description: "Implementieren Sie ein System zur Einholung und Verwaltung von Einwilligungen.",
+ Severity: SeverityWARN,
+ Category: "organizational",
+ GDPRRef: "Art. 7 DSGVO",
+ },
+ "C-PII-DETECT": {
+ ID: "C-PII-DETECT",
+ Title: "PII-Erkennung",
+ Description: "Implementieren Sie automatische Erkennung personenbezogener Daten.",
+ Severity: SeverityWARN,
+ Category: "technical",
+ GDPRRef: "Art. 32 DSGVO",
+ },
+ "C-ANONYMIZE": {
+ ID: "C-ANONYMIZE",
+ Title: "Anonymisierung/Pseudonymisierung",
+ Description: "Implementieren Sie Anonymisierung oder Pseudonymisierung vor der Verarbeitung.",
+ Severity: SeverityWARN,
+ Category: "technical",
+ GDPRRef: "Art. 32 DSGVO",
+ },
+ "C-ACCESS-CONTROL": {
+ ID: "C-ACCESS-CONTROL",
+ Title: "Zugriffskontrollen",
+ Description: "Implementieren Sie rollenbasierte Zugriffskontrollen.",
+ Severity: SeverityWARN,
+ Category: "technical",
+ GDPRRef: "Art. 32 DSGVO",
+ },
+ "C-AUDIT-LOG": {
+ ID: "C-AUDIT-LOG",
+ Title: "Audit-Logging",
+ Description: "Protokollieren Sie alle Zugriffe und Verarbeitungen.",
+ Severity: SeverityINFO,
+ Category: "technical",
+ GDPRRef: "Art. 5(2) DSGVO",
+ },
+ "C-RETENTION": {
+ ID: "C-RETENTION",
+ Title: "Aufbewahrungsfristen",
+ Description: "Definieren und implementieren Sie automatische Löschfristen.",
+ Severity: SeverityWARN,
+ Category: "organizational",
+ GDPRRef: "Art. 5(1)(e) DSGVO",
+ },
+ "C-HITL": {
+ ID: "C-HITL",
+ Title: "Human-in-the-Loop",
+ Description: "Implementieren Sie menschliche Überprüfung für KI-Entscheidungen.",
+ Severity: SeverityBLOCK,
+ Category: "organizational",
+ GDPRRef: "Art. 22 DSGVO",
+ },
+ "C-TRANSPARENCY": {
+ ID: "C-TRANSPARENCY",
+ Title: "Transparenz",
+ Description: "Informieren Sie Betroffene über KI-Verarbeitung.",
+ Severity: SeverityWARN,
+ Category: "organizational",
+ GDPRRef: "Art. 13/14 DSGVO",
+ },
+ "C-DSR-PROCESS": {
+ ID: "C-DSR-PROCESS",
+ Title: "Betroffenenrechte-Prozess",
+ Description: "Implementieren Sie Prozesse für Auskunft, Löschung, Berichtigung.",
+ Severity: SeverityWARN,
+ Category: "organizational",
+ GDPRRef: "Art. 15-22 DSGVO",
+ },
+ "C-DSFA": {
+ ID: "C-DSFA",
+ Title: "DSFA durchführen",
+ Description: "Führen Sie eine Datenschutz-Folgenabschätzung durch.",
+ Severity: SeverityWARN,
+ Category: "organizational",
+ GDPRRef: "Art. 35 DSGVO",
+ },
+ "C-SCC": {
+ ID: "C-SCC",
+ Title: "Standardvertragsklauseln",
+ Description: "Schließen Sie EU-Standardvertragsklauseln für Drittlandtransfers ab.",
+ Severity: SeverityBLOCK,
+ Category: "legal",
+ GDPRRef: "Art. 46 DSGVO",
+ },
+ "C-ENCRYPTION": {
+ ID: "C-ENCRYPTION",
+ Title: "Verschlüsselung",
+ Description: "Verschlüsseln Sie Daten in Übertragung und Speicherung.",
+ Severity: SeverityWARN,
+ Category: "technical",
+ GDPRRef: "Art. 32 DSGVO",
+ },
+ "C-MINOR-CONSENT": {
+ ID: "C-MINOR-CONSENT",
+ Title: "Elterneinwilligung",
+ Description: "Holen Sie Einwilligung der Erziehungsberechtigten ein.",
+ Severity: SeverityBLOCK,
+ Category: "organizational",
+ GDPRRef: "Art. 8 DSGVO",
+ },
+ "C-ART9-BASIS": {
+ ID: "C-ART9-BASIS",
+ Title: "Art. 9 Rechtsgrundlage",
+ Description: "Dokumentieren Sie die Rechtsgrundlage für besondere Datenkategorien.",
+ Severity: SeverityBLOCK,
+ Category: "legal",
+ GDPRRef: "Art. 9 DSGVO",
+ },
+}
+
+// GetControlByID returns a control by its ID
+func GetControlByID(id string) *RequiredControl {
+ if ctrl, exists := ControlLibrary[id]; exists {
+ return &ctrl
+ }
+ return nil
+}
diff --git a/ai-compliance-sdk/internal/ucca/rules_data.go b/ai-compliance-sdk/internal/ucca/rules_data.go
new file mode 100644
index 0000000..078ff46
--- /dev/null
+++ b/ai-compliance-sdk/internal/ucca/rules_data.go
@@ -0,0 +1,499 @@
+package ucca
+
+// AllRules contains all ~45 evaluation rules in 10 categories
+var AllRules = []Rule{
+ // =========================================================================
+ // A. Datenklassifikation (R-001 bis R-006)
+ // =========================================================================
+ {
+ Code: "R-001",
+ Category: "A. Datenklassifikation",
+ Title: "Personal Data Processing",
+ TitleDE: "Verarbeitung personenbezogener Daten",
+ Description: "Personal data is being processed",
+ DescriptionDE: "Personenbezogene Daten werden verarbeitet",
+ Severity: SeverityINFO,
+ ScoreDelta: 5,
+ GDPRRef: "Art. 4(1) DSGVO",
+ Controls: []string{"C-PII-DETECT", "C-ACCESS-CONTROL"},
+ Patterns: []string{"P-PRE-ANON"},
+ Condition: func(intake *UseCaseIntake) bool {
+ return intake.DataTypes.PersonalData
+ },
+ Rationale: func(intake *UseCaseIntake) string {
+ return "Der Use Case verarbeitet personenbezogene Daten. Dies erfordert eine Rechtsgrundlage und entsprechende Schutzmaßnahmen."
+ },
+ },
+ {
+ Code: "R-002",
+ Category: "A. Datenklassifikation",
+ Title: "Special Category Data (Art. 9)",
+ TitleDE: "Besondere Kategorien personenbezogener Daten (Art. 9)",
+ Description: "Processing of special category data requires explicit consent or legal basis",
+ DescriptionDE: "Verarbeitung besonderer Datenkategorien erfordert ausdrückliche Einwilligung oder Rechtsgrundlage",
+ Severity: SeverityWARN,
+ ScoreDelta: 20,
+ GDPRRef: "Art. 9 DSGVO",
+ Controls: []string{"C-ART9-BASIS", "C-DSFA", "C-ENCRYPTION"},
+ Patterns: []string{"P-PRE-ANON", "P-HITL-ENFORCED"},
+ Condition: func(intake *UseCaseIntake) bool {
+ return intake.DataTypes.Article9Data
+ },
+ Rationale: func(intake *UseCaseIntake) string {
+ return "Besondere Kategorien personenbezogener Daten (Gesundheit, Religion, etc.) erfordern besondere Schutzmaßnahmen und eine spezifische Rechtsgrundlage nach Art. 9 DSGVO."
+ },
+ },
+ {
+ Code: "R-003",
+ Category: "A. Datenklassifikation",
+ Title: "Minor Data Processing",
+ TitleDE: "Verarbeitung von Daten Minderjähriger",
+ Description: "Processing data of children requires special protections",
+ DescriptionDE: "Verarbeitung von Daten Minderjähriger erfordert besonderen Schutz",
+ Severity: SeverityWARN,
+ ScoreDelta: 15,
+ GDPRRef: "Art. 8 DSGVO",
+ Controls: []string{"C-MINOR-CONSENT", "C-DSFA"},
+ Patterns: []string{"P-HITL-ENFORCED"},
+ Condition: func(intake *UseCaseIntake) bool {
+ return intake.DataTypes.MinorData
+ },
+ Rationale: func(intake *UseCaseIntake) string {
+ return "Daten von Minderjährigen erfordern besonderen Schutz. Die Einwilligung muss von Erziehungsberechtigten eingeholt werden."
+ },
+ },
+ {
+ Code: "R-004",
+ Category: "A. Datenklassifikation",
+ Title: "Biometric Data",
+ TitleDE: "Biometrische Daten",
+ Description: "Biometric data processing is high risk",
+ DescriptionDE: "Verarbeitung biometrischer Daten ist hochriskant",
+ Severity: SeverityWARN,
+ ScoreDelta: 20,
+ GDPRRef: "Art. 9 DSGVO",
+ Controls: []string{"C-ART9-BASIS", "C-DSFA", "C-ENCRYPTION"},
+ Patterns: []string{"P-PRE-ANON"},
+ Condition: func(intake *UseCaseIntake) bool {
+ return intake.DataTypes.BiometricData
+ },
+ Rationale: func(intake *UseCaseIntake) string {
+ return "Biometrische Daten zur eindeutigen Identifizierung fallen unter Art. 9 DSGVO und erfordern eine DSFA."
+ },
+ },
+ {
+ Code: "R-005",
+ Category: "A. Datenklassifikation",
+ Title: "Location Data",
+ TitleDE: "Standortdaten",
+ Description: "Location tracking requires transparency and consent",
+ DescriptionDE: "Standortverfolgung erfordert Transparenz und Einwilligung",
+ Severity: SeverityINFO,
+ ScoreDelta: 10,
+ GDPRRef: "Art. 5, Art. 7 DSGVO",
+ Controls: []string{"C-CONSENT", "C-TRANSPARENCY"},
+ Patterns: []string{"P-LOG-MINIMIZATION"},
+ Condition: func(intake *UseCaseIntake) bool {
+ return intake.DataTypes.LocationData
+ },
+ Rationale: func(intake *UseCaseIntake) string {
+ return "Standortdaten ermöglichen Bewegungsprofile und erfordern klare Einwilligung und Aufbewahrungslimits."
+ },
+ },
+ {
+ Code: "R-006",
+ Category: "A. Datenklassifikation",
+ Title: "Employee Data",
+ TitleDE: "Mitarbeiterdaten",
+ Description: "Employee data processing has special considerations",
+ DescriptionDE: "Mitarbeiterdatenverarbeitung hat besondere Anforderungen",
+ Severity: SeverityINFO,
+ ScoreDelta: 10,
+ GDPRRef: "§ 26 BDSG",
+ Controls: []string{"C-ACCESS-CONTROL", "C-TRANSPARENCY"},
+ Patterns: []string{"P-NAMESPACE-ISOLATION"},
+ Condition: func(intake *UseCaseIntake) bool {
+ return intake.DataTypes.EmployeeData
+ },
+ Rationale: func(intake *UseCaseIntake) string {
+ return "Mitarbeiterdaten unterliegen zusätzlich dem BDSG § 26 und erfordern klare Zweckbindung."
+ },
+ },
+ // =========================================================================
+ // B. Zweck & Kontext (R-010 bis R-013)
+ // =========================================================================
+ {
+ Code: "R-010",
+ Category: "B. Zweck & Kontext",
+ Title: "Marketing with Personal Data",
+ TitleDE: "Marketing mit personenbezogenen Daten",
+ Description: "Marketing purposes with PII require explicit consent",
+ DescriptionDE: "Marketing mit PII erfordert ausdrückliche Einwilligung",
+ Severity: SeverityWARN,
+ ScoreDelta: 15,
+ GDPRRef: "Art. 6(1)(a) DSGVO",
+ Controls: []string{"C-CONSENT", "C-DSR-PROCESS"},
+ Patterns: []string{"P-PRE-ANON"},
+ Condition: func(intake *UseCaseIntake) bool {
+ return intake.Purpose.Marketing && intake.DataTypes.PersonalData
+ },
+ Rationale: func(intake *UseCaseIntake) string {
+ return "Marketing mit personenbezogenen Daten erfordert ausdrückliche, freiwillige Einwilligung."
+ },
+ },
+ {
+ Code: "R-011",
+ Category: "B. Zweck & Kontext",
+ Title: "Profiling Purpose",
+ TitleDE: "Profiling-Zweck",
+ Description: "Profiling requires DSFA and transparency",
+ DescriptionDE: "Profiling erfordert DSFA und Transparenz",
+ Severity: SeverityWARN,
+ ScoreDelta: 15,
+ GDPRRef: "Art. 22 DSGVO",
+ Controls: []string{"C-DSFA", "C-TRANSPARENCY", "C-DSR-PROCESS"},
+ Patterns: []string{"P-HITL-ENFORCED"},
+ Condition: func(intake *UseCaseIntake) bool {
+ return intake.Purpose.Profiling
+ },
+ Rationale: func(intake *UseCaseIntake) string {
+ return "Profiling erfordert eine DSFA und transparente Information der Betroffenen über die Logik und Auswirkungen."
+ },
+ },
+ {
+ Code: "R-012",
+ Category: "B. Zweck & Kontext",
+ Title: "Evaluation/Scoring Purpose",
+ TitleDE: "Bewertungs-/Scoring-Zweck",
+ Description: "Scoring of individuals requires safeguards",
+ DescriptionDE: "Scoring von Personen erfordert Schutzmaßnahmen",
+ Severity: SeverityWARN,
+ ScoreDelta: 15,
+ GDPRRef: "Art. 22 DSGVO",
+ Controls: []string{"C-HITL", "C-TRANSPARENCY"},
+ Patterns: []string{"P-HITL-ENFORCED"},
+ Condition: func(intake *UseCaseIntake) bool {
+ return intake.Purpose.EvaluationScoring
+ },
+ Rationale: func(intake *UseCaseIntake) string {
+ return "Bewertung/Scoring von Personen erfordert menschliche Überprüfung und Transparenz über die verwendete Logik."
+ },
+ },
+ {
+ Code: "R-013",
+ Category: "B. Zweck & Kontext",
+ Title: "Customer Support - Low Risk",
+ TitleDE: "Kundenservice - Niedriges Risiko",
+ Description: "Customer support without PII storage is low risk",
+ DescriptionDE: "Kundenservice ohne PII-Speicherung ist risikoarm",
+ Severity: SeverityINFO,
+ ScoreDelta: 0,
+ GDPRRef: "",
+ Controls: []string{},
+ Patterns: []string{"P-RAG-ONLY"},
+ Condition: func(intake *UseCaseIntake) bool {
+ return intake.Purpose.CustomerSupport && !intake.DataTypes.PersonalData
+ },
+ Rationale: func(intake *UseCaseIntake) string {
+ return "Kundenservice mit öffentlichen FAQ-Daten ohne Speicherung personenbezogener Daten ist risikoarm."
+ },
+ },
+ // =========================================================================
+ // C. Automatisierung (R-020 bis R-025)
+ // =========================================================================
+ {
+ Code: "R-020",
+ Category: "C. Automatisierung",
+ Title: "Fully Automated with Legal Effects",
+ TitleDE: "Vollautomatisiert mit rechtlichen Auswirkungen",
+ Description: "Fully automated decisions with legal effects violate Art. 22",
+ DescriptionDE: "Vollautomatisierte Entscheidungen mit rechtlichen Auswirkungen verletzen Art. 22",
+ Severity: SeverityBLOCK,
+ ScoreDelta: 40,
+ GDPRRef: "Art. 22 DSGVO",
+ Controls: []string{"C-HITL"},
+ Patterns: []string{"P-HITL-ENFORCED"},
+ Condition: func(intake *UseCaseIntake) bool {
+ return intake.Automation == AutomationFullyAutomated && intake.Outputs.LegalEffects
+ },
+ Rationale: func(intake *UseCaseIntake) string {
+ return "Vollautomatisierte Entscheidungen mit rechtlichen Auswirkungen ohne menschliche Beteiligung sind nach Art. 22 DSGVO unzulässig."
+ },
+ },
+ {
+ Code: "R-021",
+ Category: "C. Automatisierung",
+ Title: "Fully Automated Rankings/Scores",
+ TitleDE: "Vollautomatisierte Rankings/Scores",
+ Description: "Automated scoring requires human review",
+ DescriptionDE: "Automatisches Scoring erfordert menschliche Überprüfung",
+ Severity: SeverityWARN,
+ ScoreDelta: 20,
+ GDPRRef: "Art. 22 DSGVO",
+ Controls: []string{"C-HITL", "C-TRANSPARENCY"},
+ Patterns: []string{"P-HITL-ENFORCED"},
+ Condition: func(intake *UseCaseIntake) bool {
+ return intake.Automation == AutomationFullyAutomated && intake.Outputs.RankingsOrScores
+ },
+ Rationale: func(intake *UseCaseIntake) string {
+ return "Vollautomatisierte Erstellung von Rankings oder Scores erfordert menschliche Überprüfung vor Verwendung."
+ },
+ },
+ {
+ Code: "R-022",
+ Category: "C. Automatisierung",
+ Title: "Fully Automated Access Decisions",
+ TitleDE: "Vollautomatisierte Zugriffsentscheidungen",
+ Description: "Automated access decisions need safeguards",
+ DescriptionDE: "Automatisierte Zugriffsentscheidungen benötigen Schutzmaßnahmen",
+ Severity: SeverityWARN,
+ ScoreDelta: 15,
+ GDPRRef: "Art. 22 DSGVO",
+ Controls: []string{"C-HITL", "C-TRANSPARENCY", "C-DSR-PROCESS"},
+ Patterns: []string{"P-HITL-ENFORCED"},
+ Condition: func(intake *UseCaseIntake) bool {
+ return intake.Automation == AutomationFullyAutomated && intake.Outputs.AccessDecisions
+ },
+ Rationale: func(intake *UseCaseIntake) string {
+ return "Automatisierte Entscheidungen über Zugang erfordern Widerspruchsmöglichkeit und menschliche Überprüfung."
+ },
+ },
+ {
+ Code: "R-023",
+ Category: "C. Automatisierung",
+ Title: "Semi-Automated - Medium Risk",
+ TitleDE: "Teilautomatisiert - Mittleres Risiko",
+ Description: "Semi-automated processing with human review",
+ DescriptionDE: "Teilautomatisierte Verarbeitung mit menschlicher Überprüfung",
+ Severity: SeverityINFO,
+ ScoreDelta: 5,
+ GDPRRef: "",
+ Controls: []string{"C-AUDIT-LOG"},
+ Patterns: []string{"P-HITL-ENFORCED"},
+ Condition: func(intake *UseCaseIntake) bool {
+ return intake.Automation == AutomationSemiAutomated && intake.DataTypes.PersonalData
+ },
+ Rationale: func(intake *UseCaseIntake) string {
+ return "Teilautomatisierte Verarbeitung mit menschlicher Überprüfung ist grundsätzlich konform, erfordert aber Dokumentation."
+ },
+ },
+ {
+ Code: "R-024",
+ Category: "C. Automatisierung",
+ Title: "Assistive Only - Low Risk",
+ TitleDE: "Nur assistierend - Niedriges Risiko",
+ Description: "Assistive AI without automated decisions is low risk",
+ DescriptionDE: "Assistive KI ohne automatisierte Entscheidungen ist risikoarm",
+ Severity: SeverityINFO,
+ ScoreDelta: 0,
+ GDPRRef: "",
+ Controls: []string{},
+ Patterns: []string{"P-RAG-ONLY"},
+ Condition: func(intake *UseCaseIntake) bool {
+ return intake.Automation == AutomationAssistive
+ },
+ Rationale: func(intake *UseCaseIntake) string {
+ return "Rein assistive KI, die nur Vorschläge macht und keine Entscheidungen trifft, ist risikoarm."
+ },
+ },
+ {
+ Code: "R-025",
+ Category: "C. Automatisierung",
+ Title: "HR Scoring - Blocked",
+ TitleDE: "HR-Scoring - Blockiert",
+ Description: "Automated HR scoring/evaluation is prohibited",
+ DescriptionDE: "Automatisiertes HR-Scoring/Bewertung ist verboten",
+ Severity: SeverityBLOCK,
+ ScoreDelta: 50,
+ GDPRRef: "Art. 22, § 26 BDSG",
+ Controls: []string{"C-HITL"},
+ Patterns: []string{},
+ Condition: func(intake *UseCaseIntake) bool {
+ return intake.Domain == DomainHR &&
+ intake.Purpose.EvaluationScoring &&
+ intake.Automation == AutomationFullyAutomated
+ },
+ Rationale: func(intake *UseCaseIntake) string {
+ return "Vollautomatisierte Bewertung/Scoring von Mitarbeitern ist unzulässig. Arbeitsrechtliche Entscheidungen müssen von Menschen getroffen werden."
+ },
+ },
+ // =========================================================================
+ // D. Training vs Nutzung (R-030 bis R-035)
+ // =========================================================================
+ {
+ Code: "R-030",
+ Category: "D. Training vs Nutzung",
+ Title: "Training with Personal Data",
+ TitleDE: "Training mit personenbezogenen Daten",
+ Description: "Training AI with personal data is high risk",
+ DescriptionDE: "Training von KI mit personenbezogenen Daten ist hochriskant",
+ Severity: SeverityBLOCK,
+ ScoreDelta: 40,
+ GDPRRef: "Art. 5(1)(b)(c) DSGVO",
+ Controls: []string{"C-ART9-BASIS", "C-DSFA"},
+ Patterns: []string{"P-RAG-ONLY"},
+ Condition: func(intake *UseCaseIntake) bool {
+ return intake.ModelUsage.Training && intake.DataTypes.PersonalData
+ },
+ Rationale: func(intake *UseCaseIntake) string {
+ return "Training von KI-Modellen mit personenbezogenen Daten verstößt gegen Zweckbindung und Datenminimierung. Nutzen Sie stattdessen RAG."
+ },
+ },
+ {
+ Code: "R-031",
+ Category: "D. Training vs Nutzung",
+ Title: "Fine-tuning with Personal Data",
+ TitleDE: "Fine-Tuning mit personenbezogenen Daten",
+ Description: "Fine-tuning with PII requires safeguards",
+ DescriptionDE: "Fine-Tuning mit PII erfordert Schutzmaßnahmen",
+ Severity: SeverityWARN,
+ ScoreDelta: 25,
+ GDPRRef: "Art. 5(1)(b)(c) DSGVO",
+ Controls: []string{"C-ANONYMIZE", "C-DSFA"},
+ Patterns: []string{"P-PRE-ANON"},
+ Condition: func(intake *UseCaseIntake) bool {
+ return intake.ModelUsage.Finetune && intake.DataTypes.PersonalData
+ },
+ Rationale: func(intake *UseCaseIntake) string {
+ return "Fine-Tuning mit personenbezogenen Daten ist nur nach Anonymisierung/Pseudonymisierung zulässig."
+ },
+ },
+ {
+ Code: "R-032",
+ Category: "D. Training vs Nutzung",
+ Title: "RAG Only - Recommended",
+ TitleDE: "Nur RAG - Empfohlen",
+ Description: "RAG without training is the safest approach",
+ DescriptionDE: "RAG ohne Training ist der sicherste Ansatz",
+ Severity: SeverityINFO,
+ ScoreDelta: 0,
+ GDPRRef: "",
+ Controls: []string{},
+ Patterns: []string{"P-RAG-ONLY"},
+ Condition: func(intake *UseCaseIntake) bool {
+ return intake.ModelUsage.RAG && !intake.ModelUsage.Training && !intake.ModelUsage.Finetune
+ },
+ Rationale: func(intake *UseCaseIntake) string {
+ return "Nur-RAG ohne Training oder Fine-Tuning ist die empfohlene Architektur für DSGVO-Konformität."
+ },
+ },
+ {
+ Code: "R-033",
+ Category: "D. Training vs Nutzung",
+ Title: "Training with Article 9 Data",
+ TitleDE: "Training mit Art. 9 Daten",
+ Description: "Training with special category data is prohibited",
+ DescriptionDE: "Training mit besonderen Datenkategorien ist verboten",
+ Severity: SeverityBLOCK,
+ ScoreDelta: 50,
+ GDPRRef: "Art. 9 DSGVO",
+ Controls: []string{},
+ Patterns: []string{},
+ Condition: func(intake *UseCaseIntake) bool {
+ return (intake.ModelUsage.Training || intake.ModelUsage.Finetune) && intake.DataTypes.Article9Data
+ },
+ Rationale: func(intake *UseCaseIntake) string {
+ return "Training oder Fine-Tuning mit besonderen Kategorien personenbezogener Daten (Gesundheit, Religion, etc.) ist grundsätzlich unzulässig."
+ },
+ },
+ {
+ Code: "R-034",
+ Category: "D. Training vs Nutzung",
+ Title: "Inference with Public Data",
+ TitleDE: "Inferenz mit öffentlichen Daten",
+ Description: "Using only public data is low risk",
+ DescriptionDE: "Nutzung nur öffentlicher Daten ist risikoarm",
+ Severity: SeverityINFO,
+ ScoreDelta: 0,
+ GDPRRef: "",
+ Controls: []string{},
+ Patterns: []string{},
+ Condition: func(intake *UseCaseIntake) bool {
+ return intake.DataTypes.PublicData && !intake.DataTypes.PersonalData
+ },
+ Rationale: func(intake *UseCaseIntake) string {
+ return "Die ausschließliche Nutzung öffentlich zugänglicher Daten ohne Personenbezug ist unproblematisch."
+ },
+ },
+ {
+ Code: "R-035",
+ Category: "D. Training vs Nutzung",
+ Title: "Training with Minor Data",
+ TitleDE: "Training mit Daten Minderjähriger",
+ Description: "Training with children's data is prohibited",
+ DescriptionDE: "Training mit Kinderdaten ist verboten",
+ Severity: SeverityBLOCK,
+ ScoreDelta: 50,
+ GDPRRef: "Art. 8 DSGVO, ErwG 38",
+ Controls: []string{},
+ Patterns: []string{},
+ Condition: func(intake *UseCaseIntake) bool {
+ return (intake.ModelUsage.Training || intake.ModelUsage.Finetune) && intake.DataTypes.MinorData
+ },
+ Rationale: func(intake *UseCaseIntake) string {
+ return "Training von KI-Modellen mit Daten von Minderjährigen ist aufgrund des besonderen Schutzes unzulässig."
+ },
+ },
+ // =========================================================================
+ // E. Speicherung (R-040 bis R-042)
+ // =========================================================================
+ {
+ Code: "R-040",
+ Category: "E. Speicherung",
+ Title: "Storing Prompts with PII",
+ TitleDE: "Speicherung von Prompts mit PII",
+ Description: "Storing prompts containing PII requires controls",
+ DescriptionDE: "Speicherung von Prompts mit PII erfordert Kontrollen",
+ Severity: SeverityWARN,
+ ScoreDelta: 15,
+ GDPRRef: "Art. 5(1)(e) DSGVO",
+ Controls: []string{"C-RETENTION", "C-ANONYMIZE", "C-DSR-PROCESS"},
+ Patterns: []string{"P-LOG-MINIMIZATION", "P-PRE-ANON"},
+ Condition: func(intake *UseCaseIntake) bool {
+ return intake.Retention.StorePrompts && intake.DataTypes.PersonalData
+ },
+ Rationale: func(intake *UseCaseIntake) string {
+ return "Speicherung von Prompts mit personenbezogenen Daten erfordert Löschfristen und Anonymisierungsoptionen."
+ },
+ },
+ {
+ Code: "R-041",
+ Category: "E. Speicherung",
+ Title: "Storing Responses with PII",
+ TitleDE: "Speicherung von Antworten mit PII",
+ Description: "Storing AI responses containing PII requires controls",
+ DescriptionDE: "Speicherung von KI-Antworten mit PII erfordert Kontrollen",
+ Severity: SeverityWARN,
+ ScoreDelta: 10,
+ GDPRRef: "Art. 5(1)(e) DSGVO",
+ Controls: []string{"C-RETENTION", "C-DSR-PROCESS"},
+ Patterns: []string{"P-LOG-MINIMIZATION"},
+ Condition: func(intake *UseCaseIntake) bool {
+ return intake.Retention.StoreResponses && intake.DataTypes.PersonalData
+ },
+ Rationale: func(intake *UseCaseIntake) string {
+ return "Speicherung von KI-Antworten mit personenbezogenen Daten erfordert definierte Aufbewahrungsfristen."
+ },
+ },
+ {
+ Code: "R-042",
+ Category: "E. Speicherung",
+ Title: "No Retention Policy",
+ TitleDE: "Keine Aufbewahrungsrichtlinie",
+ Description: "PII storage without retention limits is problematic",
+ DescriptionDE: "PII-Speicherung ohne Aufbewahrungslimits ist problematisch",
+ Severity: SeverityWARN,
+ ScoreDelta: 10,
+ GDPRRef: "Art. 5(1)(e) DSGVO",
+ Controls: []string{"C-RETENTION"},
+ Patterns: []string{"P-LOG-MINIMIZATION"},
+ Condition: func(intake *UseCaseIntake) bool {
+ return (intake.Retention.StorePrompts || intake.Retention.StoreResponses) &&
+ intake.DataTypes.PersonalData &&
+ intake.Retention.RetentionDays == 0
+ },
+ Rationale: func(intake *UseCaseIntake) string {
+ return "Speicherung personenbezogener Daten ohne definierte Aufbewahrungsfrist verstößt gegen den Grundsatz der Speicherbegrenzung."
+ },
+ },
+}
diff --git a/ai-compliance-sdk/internal/ucca/rules_data_fj.go b/ai-compliance-sdk/internal/ucca/rules_data_fj.go
new file mode 100644
index 0000000..6106ac0
--- /dev/null
+++ b/ai-compliance-sdk/internal/ucca/rules_data_fj.go
@@ -0,0 +1,323 @@
+package ucca
+
+func init() {
+ AllRules = append(AllRules, rulesFJ()...)
+}
+
+// rulesFJ returns rules for categories F–J (R-050 to R-100)
+func rulesFJ() []Rule {
+ return []Rule{
+ // =========================================================================
+ // F. Hosting (R-050 bis R-052)
+ // =========================================================================
+ {
+ Code: "R-050",
+ Category: "F. Hosting",
+ Title: "Third Country Transfer with PII",
+ TitleDE: "Drittlandtransfer mit PII",
+ Description: "Transferring PII to third countries requires safeguards",
+ DescriptionDE: "Übermittlung von PII in Drittländer erfordert Schutzmaßnahmen",
+ Severity: SeverityWARN,
+ ScoreDelta: 20,
+ GDPRRef: "Art. 44-49 DSGVO",
+ Controls: []string{"C-SCC", "C-ENCRYPTION"},
+ Patterns: []string{},
+ Condition: func(intake *UseCaseIntake) bool {
+ return intake.Hosting.Region == "third_country" && intake.DataTypes.PersonalData
+ },
+ Rationale: func(intake *UseCaseIntake) string {
+ return "Übermittlung personenbezogener Daten in Drittländer erfordert Standardvertragsklauseln oder andere geeignete Garantien."
+ },
+ },
+ {
+ Code: "R-051",
+ Category: "F. Hosting",
+ Title: "EU Hosting - Compliant",
+ TitleDE: "EU-Hosting - Konform",
+ Description: "Hosting within EU is compliant with GDPR",
+ DescriptionDE: "Hosting innerhalb der EU ist DSGVO-konform",
+ Severity: SeverityINFO,
+ ScoreDelta: 0,
+ GDPRRef: "",
+ Controls: []string{},
+ Patterns: []string{},
+ Condition: func(intake *UseCaseIntake) bool {
+ return intake.Hosting.Region == "eu"
+ },
+ Rationale: func(intake *UseCaseIntake) string {
+ return "Hosting innerhalb der EU/EWR erfüllt grundsätzlich die DSGVO-Anforderungen an den Datenstandort."
+ },
+ },
+ {
+ Code: "R-052",
+ Category: "F. Hosting",
+ Title: "On-Premise Hosting",
+ TitleDE: "On-Premise-Hosting",
+ Description: "On-premise hosting gives most control",
+ DescriptionDE: "On-Premise-Hosting gibt die meiste Kontrolle",
+ Severity: SeverityINFO,
+ ScoreDelta: 0,
+ GDPRRef: "",
+ Controls: []string{"C-ENCRYPTION"},
+ Patterns: []string{},
+ Condition: func(intake *UseCaseIntake) bool {
+ return intake.Hosting.Region == "on_prem"
+ },
+ Rationale: func(intake *UseCaseIntake) string {
+ return "On-Premise-Hosting bietet maximale Kontrolle über Daten, erfordert aber eigene Sicherheitsmaßnahmen."
+ },
+ },
+ // =========================================================================
+ // G. Transparenz (R-060 bis R-062)
+ // =========================================================================
+ {
+ Code: "R-060",
+ Category: "G. Transparenz",
+ Title: "No Human Review for Decisions",
+ TitleDE: "Keine menschliche Überprüfung bei Entscheidungen",
+ Description: "Decisions affecting individuals need human review option",
+ DescriptionDE: "Entscheidungen, die Personen betreffen, benötigen menschliche Überprüfungsoption",
+ Severity: SeverityWARN,
+ ScoreDelta: 15,
+ GDPRRef: "Art. 22(3) DSGVO",
+ Controls: []string{"C-HITL", "C-DSR-PROCESS"},
+ Patterns: []string{"P-HITL-ENFORCED"},
+ Condition: func(intake *UseCaseIntake) bool {
+ return (intake.Outputs.LegalEffects || intake.Outputs.AccessDecisions || intake.Purpose.DecisionMaking) &&
+ intake.Automation != AutomationAssistive
+ },
+ Rationale: func(intake *UseCaseIntake) string {
+ return "Betroffene haben das Recht auf menschliche Überprüfung bei automatisierten Entscheidungen."
+ },
+ },
+ {
+ Code: "R-061",
+ Category: "G. Transparenz",
+ Title: "External Recommendations",
+ TitleDE: "Externe Empfehlungen",
+ Description: "Recommendations to users need transparency",
+ DescriptionDE: "Empfehlungen an Nutzer erfordern Transparenz",
+ Severity: SeverityINFO,
+ ScoreDelta: 5,
+ GDPRRef: "Art. 13/14 DSGVO",
+ Controls: []string{"C-TRANSPARENCY"},
+ Patterns: []string{},
+ Condition: func(intake *UseCaseIntake) bool {
+ return intake.Outputs.RecommendationsToUsers && intake.DataTypes.PersonalData
+ },
+ Rationale: func(intake *UseCaseIntake) string {
+ return "Personalisierte Empfehlungen erfordern Information der Nutzer über die KI-Verarbeitung."
+ },
+ },
+ {
+ Code: "R-062",
+ Category: "G. Transparenz",
+ Title: "Content Generation without Disclosure",
+ TitleDE: "Inhaltsgenerierung ohne Offenlegung",
+ Description: "AI-generated content should be disclosed",
+ DescriptionDE: "KI-generierte Inhalte sollten offengelegt werden",
+ Severity: SeverityINFO,
+ ScoreDelta: 5,
+ GDPRRef: "EU-AI-Act Art. 52",
+ Controls: []string{"C-TRANSPARENCY"},
+ Patterns: []string{},
+ Condition: func(intake *UseCaseIntake) bool {
+ return intake.Outputs.ContentGeneration
+ },
+ Rationale: func(intake *UseCaseIntake) string {
+ return "KI-generierte Inhalte sollten als solche gekennzeichnet werden (EU-AI-Act Transparenzpflicht)."
+ },
+ },
+ // =========================================================================
+ // H. Domain-spezifisch (R-070 bis R-074)
+ // =========================================================================
+ {
+ Code: "R-070",
+ Category: "H. Domain-spezifisch",
+ Title: "Education + Scoring = Blocked",
+ TitleDE: "Bildung + Scoring = Blockiert",
+ Description: "Automated scoring of students is prohibited",
+ DescriptionDE: "Automatisches Scoring von Schülern ist verboten",
+ Severity: SeverityBLOCK,
+ ScoreDelta: 50,
+ GDPRRef: "Art. 8, Art. 22 DSGVO",
+ Controls: []string{},
+ Patterns: []string{},
+ Condition: func(intake *UseCaseIntake) bool {
+ return intake.Domain == DomainEducation &&
+ intake.DataTypes.MinorData &&
+ (intake.Purpose.EvaluationScoring || intake.Outputs.RankingsOrScores)
+ },
+ Rationale: func(intake *UseCaseIntake) string {
+ return "Automatisches Scoring oder Ranking von Schülern/Minderjährigen ist aufgrund des besonderen Schutzes unzulässig."
+ },
+ },
+ {
+ Code: "R-071",
+ Category: "H. Domain-spezifisch",
+ Title: "Healthcare + Automated Diagnosis",
+ TitleDE: "Gesundheit + Automatische Diagnose",
+ Description: "Automated medical decisions require strict controls",
+ DescriptionDE: "Automatische medizinische Entscheidungen erfordern strenge Kontrollen",
+ Severity: SeverityBLOCK,
+ ScoreDelta: 45,
+ GDPRRef: "Art. 9, Art. 22 DSGVO",
+ Controls: []string{"C-HITL", "C-DSFA", "C-ART9-BASIS"},
+ Patterns: []string{"P-HITL-ENFORCED"},
+ Condition: func(intake *UseCaseIntake) bool {
+ return intake.Domain == DomainHealthcare &&
+ intake.Automation == AutomationFullyAutomated &&
+ intake.Purpose.DecisionMaking
+ },
+ Rationale: func(intake *UseCaseIntake) string {
+ return "Vollautomatisierte medizinische Diagnosen oder Behandlungsentscheidungen sind ohne ärztliche Überprüfung unzulässig."
+ },
+ },
+ {
+ Code: "R-072",
+ Category: "H. Domain-spezifisch",
+ Title: "Finance + Automated Credit Scoring",
+ TitleDE: "Finanzen + Automatisches Credit-Scoring",
+ Description: "Automated credit decisions require transparency",
+ DescriptionDE: "Automatische Kreditentscheidungen erfordern Transparenz",
+ Severity: SeverityWARN,
+ ScoreDelta: 20,
+ GDPRRef: "Art. 22 DSGVO",
+ Controls: []string{"C-HITL", "C-TRANSPARENCY", "C-DSR-PROCESS"},
+ Patterns: []string{"P-HITL-ENFORCED"},
+ Condition: func(intake *UseCaseIntake) bool {
+ return intake.Domain == DomainFinance &&
+ (intake.Purpose.EvaluationScoring || intake.Outputs.RankingsOrScores) &&
+ intake.DataTypes.FinancialData
+ },
+ Rationale: func(intake *UseCaseIntake) string {
+ return "Automatische Kreditwürdigkeitsprüfung erfordert Erklärbarkeit und Widerspruchsmöglichkeit."
+ },
+ },
+ {
+ Code: "R-073",
+ Category: "H. Domain-spezifisch",
+ Title: "Utilities + RAG Chatbot = Low Risk",
+ TitleDE: "Versorgungsunternehmen + RAG-Chatbot = Niedriges Risiko",
+ Description: "RAG-based customer service chatbot is low risk",
+ DescriptionDE: "RAG-basierter Kundenservice-Chatbot ist risikoarm",
+ Severity: SeverityINFO,
+ ScoreDelta: 0,
+ GDPRRef: "",
+ Controls: []string{},
+ Patterns: []string{"P-RAG-ONLY"},
+ Condition: func(intake *UseCaseIntake) bool {
+ return intake.Domain == DomainUtilities &&
+ intake.ModelUsage.RAG &&
+ intake.Purpose.CustomerSupport &&
+ !intake.DataTypes.PersonalData
+ },
+ Rationale: func(intake *UseCaseIntake) string {
+ return "Ein RAG-basierter Kundenservice-Chatbot ohne Speicherung personenbezogener Daten ist ein Best-Practice-Beispiel."
+ },
+ },
+ {
+ Code: "R-074",
+ Category: "H. Domain-spezifisch",
+ Title: "Public Sector + Automated Decisions",
+ TitleDE: "Öffentlicher Sektor + Automatische Entscheidungen",
+ Description: "Public sector automated decisions need special care",
+ DescriptionDE: "Automatische Entscheidungen im öffentlichen Sektor erfordern besondere Sorgfalt",
+ Severity: SeverityWARN,
+ ScoreDelta: 20,
+ GDPRRef: "Art. 22 DSGVO",
+ Controls: []string{"C-HITL", "C-TRANSPARENCY", "C-DSFA"},
+ Patterns: []string{"P-HITL-ENFORCED"},
+ Condition: func(intake *UseCaseIntake) bool {
+ return intake.Domain == DomainPublic &&
+ intake.Purpose.DecisionMaking &&
+ intake.Automation != AutomationAssistive
+ },
+ Rationale: func(intake *UseCaseIntake) string {
+ return "Verwaltungsentscheidungen, die Bürger betreffen, erfordern besondere Transparenz und Überprüfungsmöglichkeiten."
+ },
+ },
+ // =========================================================================
+ // I. Aggregation (R-090 bis R-092) - Implicit in Evaluate()
+ // =========================================================================
+ {
+ Code: "R-090",
+ Category: "I. Aggregation",
+ Title: "Block Rules Triggered",
+ TitleDE: "Blockierungsregeln ausgelöst",
+ Description: "Any BLOCK severity results in NO feasibility",
+ DescriptionDE: "Jede BLOCK-Schwere führt zu NEIN-Machbarkeit",
+ Severity: SeverityBLOCK,
+ ScoreDelta: 0,
+ GDPRRef: "",
+ Controls: []string{},
+ Patterns: []string{},
+ Condition: func(intake *UseCaseIntake) bool {
+ return false // handled in aggregation logic
+ },
+ Rationale: func(intake *UseCaseIntake) string {
+ return "Eine oder mehrere kritische Regelverletzungen führen zur Einstufung als nicht umsetzbar."
+ },
+ },
+ {
+ Code: "R-091",
+ Category: "I. Aggregation",
+ Title: "Warning Rules Only",
+ TitleDE: "Nur Warnungsregeln",
+ Description: "Only WARN severity results in CONDITIONAL",
+ DescriptionDE: "Nur WARN-Schwere führt zu BEDINGT",
+ Severity: SeverityWARN,
+ ScoreDelta: 0,
+ GDPRRef: "",
+ Controls: []string{},
+ Patterns: []string{},
+ Condition: func(intake *UseCaseIntake) bool {
+ return false // handled in aggregation logic
+ },
+ Rationale: func(intake *UseCaseIntake) string {
+ return "Warnungen erfordern Maßnahmen, blockieren aber nicht die Umsetzung."
+ },
+ },
+ {
+ Code: "R-092",
+ Category: "I. Aggregation",
+ Title: "Info Only - Clear Path",
+ TitleDE: "Nur Info - Freier Weg",
+ Description: "Only INFO severity results in YES",
+ DescriptionDE: "Nur INFO-Schwere führt zu JA",
+ Severity: SeverityINFO,
+ ScoreDelta: 0,
+ GDPRRef: "",
+ Controls: []string{},
+ Patterns: []string{},
+ Condition: func(intake *UseCaseIntake) bool {
+ return false // handled in aggregation logic
+ },
+ Rationale: func(intake *UseCaseIntake) string {
+ return "Keine kritischen oder warnenden Regeln ausgelöst - Umsetzung empfohlen."
+ },
+ },
+ // =========================================================================
+ // J. Erklärung (R-100)
+ // =========================================================================
+ {
+ Code: "R-100",
+ Category: "J. Erklärung",
+ Title: "Rejection Must Include Reason and Alternative",
+ TitleDE: "Ablehnung muss Begründung und Alternative enthalten",
+ Description: "When feasibility is NO, provide reason and alternative",
+ DescriptionDE: "Bei Machbarkeit NEIN, Begründung und Alternative angeben",
+ Severity: SeverityINFO,
+ ScoreDelta: 0,
+ GDPRRef: "",
+ Controls: []string{},
+ Patterns: []string{},
+ Condition: func(intake *UseCaseIntake) bool {
+ return false // handled in summary generation
+ },
+ Rationale: func(intake *UseCaseIntake) string {
+ return "Jede Ablehnung enthält eine klare Begründung und einen alternativen Ansatz."
+ },
+ },
+ }
+}