refactor(go): split training/store, ucca/rules, ucca_handlers, document_export under 500 LOC

Each of the four oversized files (training/store.go 1569 LOC, ucca/rules.go 1231 LOC,
ucca_handlers.go 1135 LOC, document_export.go 1101 LOC) is split by logical group
into same-package files, all under the 500-line hard cap. Zero behavior changes,
no renamed exported symbols. Also fixed pre-existing hazard_library split (missing
functions and duplicate UUID keys from a prior session).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Sharang Parnerkar
2026-04-19 09:29:54 +02:00
parent 3f306fb6f0
commit 9f96061631
36 changed files with 9416 additions and 9365 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -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",
},
})
}

View File

@@ -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] + "..."
}

View File

@@ -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
}

File diff suppressed because it is too large Load Diff

View File

@@ -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 := `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">
<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>
<Default Extension="xml" ContentType="application/xml"/>
<Override PartName="/word/document.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml"/>
</Types>`
if err := addZipEntry(zw, "[Content_Types].xml", contentTypes); err != nil {
return nil, fmt.Errorf("failed to write [Content_Types].xml: %w", err)
}
rels := `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="word/document.xml"/>
</Relationships>`
if err := addZipEntry(zw, "_rels/.rels", rels); err != nil {
return nil, fmt.Errorf("failed to write _rels/.rels: %w", err)
}
docRels := `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
</Relationships>`
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(`<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<w:document xmlns:wpc="http://schemas.microsoft.com/office/word/2010/wordprocessingCanvas"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:o="urn:schemas-microsoft-com:office:office"
xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"
xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math"
xmlns:v="urn:schemas-microsoft-com:vml"
xmlns:wp="http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing"
xmlns:w10="urn:schemas-microsoft-com:office:word"
xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"
xmlns:w14="http://schemas.microsoft.com/office/word/2010/wordml"
xmlns:wpg="http://schemas.microsoft.com/office/word/2010/wordprocessingGroup"
xmlns:wpi="http://schemas.microsoft.com/office/word/2010/wordprocessingInk"
xmlns:wne="http://schemas.microsoft.com/office/word/2006/wordml"
xmlns:wps="http://schemas.microsoft.com/office/word/2010/wordprocessingShape"
mc:Ignorable="w14 wp14">
<w:body>
%s
</w:body>
</w:document>`, 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
}

View File

@@ -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))
}
}

View File

@@ -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(` <w:p>
<w:pPr>
<w:pStyle w:val="Heading%d"/>
<w:spacing w:after="200"/>
</w:pPr>
<w:r>
<w:rPr><w:b/><w:sz w:val="%d"/><w:szCs w:val="%d"/></w:rPr>
<w:t xml:space="preserve">%s</w:t>
</w:r>
</w:p>
`, 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 = "<w:rPr><w:i/></w:rPr>"
}
return fmt.Sprintf(` <w:p>
<w:r>
%s
<w:t xml:space="preserve">%s</w:t>
</w:r>
</w:p>
`, rpr, escaped)
}

View File

@@ -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)
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -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,
},
}
}

View File

@@ -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,
},
}
}

View File

@@ -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,
},
}
}

View File

@@ -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,
},
}
}

View File

@@ -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,
},
}
}

View File

@@ -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,
},
}
}

View File

@@ -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,
},
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -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
}

View File

@@ -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, &regulationReferences,
&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, &regulationReferences,
&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
}

View File

@@ -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
}

View File

@@ -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, &reg, &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, &reg, &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
}

File diff suppressed because it is too large Load Diff

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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,
&regulationArea, &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,
&regulationArea, &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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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."
},
},
}

View File

@@ -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
}

View File

@@ -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."
},
},
}

View File

@@ -0,0 +1,323 @@
package ucca
func init() {
AllRules = append(AllRules, rulesFJ()...)
}
// rulesFJ returns rules for categories FJ (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."
},
},
}
}