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", "Regelmae­ssige 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", "Regelmae­ssige 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{"Schliess­kantensicherung 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{"Schliess­kraftbegrenzung", "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", "Beruerungs­sichere Klemmleisten"}, - RecommendedMeasuresTechnical: []string{"Lichtbogen-Erkennungssystem", "Fernausloesemoeglich­keit"}, - 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 Abschaltzeit­nachweis"}), - 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 Schulterhoe­he"}, - 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{"Kameraueberwa­chung 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, Gebaeudezersto­erung", - 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 Gesundheitsge­faehrdung 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", "Beruerungs­sichere Klemmleisten"}, + RecommendedMeasuresTechnical: []string{"Lichtbogen-Erkennungssystem", "Fernausloesemoeglich­keit"}, + 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 Abschaltzeit­nachweis"}), + 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 Schulterhoe­he"}, + 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{"Kameraueberwa­chung 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, Gebaeudezersto­erung", + 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 Gesundheitsge­faehrdung 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", "Regelmae­ssige 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", "Regelmae­ssige 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{"Schliess­kantensicherung 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{"Schliess­kraftbegrenzung", "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." + }, + }, + } +}