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:
File diff suppressed because it is too large
Load Diff
203
ai-compliance-sdk/internal/api/handlers/ucca_handlers_catalog.go
Normal file
203
ai-compliance-sdk/internal/api/handlers/ucca_handlers_catalog.go
Normal 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",
|
||||
},
|
||||
})
|
||||
}
|
||||
243
ai-compliance-sdk/internal/api/handlers/ucca_handlers_explain.go
Normal file
243
ai-compliance-sdk/internal/api/handlers/ucca_handlers_explain.go
Normal 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] + "..."
|
||||
}
|
||||
280
ai-compliance-sdk/internal/api/handlers/ucca_handlers_wizard.go
Normal file
280
ai-compliance-sdk/internal/api/handlers/ucca_handlers_wizard.go
Normal 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
161
ai-compliance-sdk/internal/iace/document_export_docx_json.go
Normal file
161
ai-compliance-sdk/internal/iace/document_export_docx_json.go
Normal 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
|
||||
}
|
||||
261
ai-compliance-sdk/internal/iace/document_export_excel.go
Normal file
261
ai-compliance-sdk/internal/iace/document_export_excel.go
Normal 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))
|
||||
}
|
||||
}
|
||||
198
ai-compliance-sdk/internal/iace/document_export_helpers.go
Normal file
198
ai-compliance-sdk/internal/iace/document_export_helpers.go
Normal 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)
|
||||
}
|
||||
313
ai-compliance-sdk/internal/iace/document_export_pdf.go
Normal file
313
ai-compliance-sdk/internal/iace/document_export_pdf.go
Normal 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
580
ai-compliance-sdk/internal/iace/hazard_library_ai_sw.go
Normal file
580
ai-compliance-sdk/internal/iace/hazard_library_ai_sw.go
Normal 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,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,217 @@
|
||||
package iace
|
||||
|
||||
import "time"
|
||||
|
||||
// builtinHazardsISO12100ElectricalThermal returns ISO 12100 electrical hazard
|
||||
// entries (indices 7-10) and thermal hazard entries (indices 5-8).
|
||||
func builtinHazardsISO12100ElectricalThermal() []HazardLibraryEntry {
|
||||
now := time.Now()
|
||||
return []HazardLibraryEntry{
|
||||
// ====================================================================
|
||||
// Category: electrical_hazard (indices 7-10, 4 entries)
|
||||
// ====================================================================
|
||||
{
|
||||
ID: hazardUUID("electrical_hazard", 7),
|
||||
Category: "electrical_hazard",
|
||||
SubCategory: "lichtbogen",
|
||||
Name: "Lichtbogengefahr bei Schalthandlungen",
|
||||
Description: "Beim Schalten unter Last kann ein Lichtbogen entstehen, der zu Verbrennungen und Augenschaeden fuehrt.",
|
||||
DefaultSeverity: 5,
|
||||
DefaultProbability: 2,
|
||||
DefaultExposure: 2,
|
||||
DefaultAvoidance: 2,
|
||||
ApplicableComponentTypes: []string{"electrical"},
|
||||
RegulationReferences: []string{"Maschinenverordnung 2023/1230 Anhang I"},
|
||||
SuggestedMitigations: mustMarshalJSON([]string{"Lichtbogenschutzkleidung (PSA)", "Fernbediente Schaltgeraete"}),
|
||||
TypicalCauses: []string{"Schalten unter Last", "Verschmutzte Kontakte", "Fehlbedienung bei Wartung"},
|
||||
TypicalHarm: "Verbrennungen durch Lichtbogen, Augenschaeden, Druckwelle",
|
||||
RelevantLifecyclePhases: []string{"maintenance", "fault_finding"},
|
||||
RecommendedMeasuresDesign: []string{"Lasttrennschalter mit Lichtbogenkammer", "Beruerungssichere Klemmleisten"},
|
||||
RecommendedMeasuresTechnical: []string{"Lichtbogen-Erkennungssystem", "Fernausloesemoeglichkeit"},
|
||||
RecommendedMeasuresInformation: []string{"PSA-Pflicht bei Schalthandlungen", "Schaltbefugnisregelung"},
|
||||
SuggestedEvidence: []string{"Lichtbogenberechnung", "PSA-Ausstattungsnachweis"},
|
||||
RelatedKeywords: []string{"Lichtbogen", "Schalthandlung", "Arc Flash"},
|
||||
IsBuiltin: true,
|
||||
TenantID: nil,
|
||||
CreatedAt: now,
|
||||
},
|
||||
{
|
||||
ID: hazardUUID("electrical_hazard", 8),
|
||||
Category: "electrical_hazard",
|
||||
SubCategory: "ueberstrom",
|
||||
Name: "Ueberstrom durch Kurzschluss",
|
||||
Description: "Ein Kurzschluss kann zu extrem hohen Stroemen fuehren, die Leitungen ueberhitzen, Braende ausloesen und Bauteile zerstoeren.",
|
||||
DefaultSeverity: 4,
|
||||
DefaultProbability: 2,
|
||||
DefaultExposure: 2,
|
||||
DefaultAvoidance: 3,
|
||||
ApplicableComponentTypes: []string{"electrical"},
|
||||
RegulationReferences: []string{"Maschinenverordnung 2023/1230 Anhang I"},
|
||||
SuggestedMitigations: mustMarshalJSON([]string{"Selektive Absicherung mit Schmelzsicherungen", "Kurzschlussberechnung und Abschaltzeitnachweis"}),
|
||||
TypicalCauses: []string{"Beschaedigte Leitungsisolierung", "Feuchtigkeitseintritt", "Fehlerhafte Verdrahtung"},
|
||||
TypicalHarm: "Brandgefahr, Zerstoerung elektrischer Betriebsmittel",
|
||||
RelevantLifecyclePhases: []string{"normal_operation", "maintenance", "installation"},
|
||||
RecommendedMeasuresDesign: []string{"Kurzschlussfeste Dimensionierung der Leitungen", "Selektive Schutzkoordination"},
|
||||
RecommendedMeasuresTechnical: []string{"Leitungsschutzschalter", "Fehlerstrom-Schutzeinrichtung"},
|
||||
RecommendedMeasuresInformation: []string{"Stromlaufplan aktuell halten", "Prueffristen fuer elektrische Anlage"},
|
||||
SuggestedEvidence: []string{"Kurzschlussberechnung", "Pruefprotokoll nach DGUV V3"},
|
||||
RelatedKeywords: []string{"Kurzschluss", "Ueberstrom", "Leitungsschutz"},
|
||||
IsBuiltin: true,
|
||||
TenantID: nil,
|
||||
CreatedAt: now,
|
||||
},
|
||||
{
|
||||
ID: hazardUUID("electrical_hazard", 9),
|
||||
Category: "electrical_hazard",
|
||||
SubCategory: "erdungsfehler",
|
||||
Name: "Erdungsfehler im Schutzleitersystem",
|
||||
Description: "Ein unterbrochener oder fehlerhafter Schutzleiter verhindert die sichere Ableitung von Fehlerstroemen und macht Gehaeuse spannungsfuehrend.",
|
||||
DefaultSeverity: 5,
|
||||
DefaultProbability: 2,
|
||||
DefaultExposure: 3,
|
||||
DefaultAvoidance: 2,
|
||||
ApplicableComponentTypes: []string{"electrical"},
|
||||
RegulationReferences: []string{"Maschinenverordnung 2023/1230 Anhang I"},
|
||||
SuggestedMitigations: mustMarshalJSON([]string{"Regelmaessige Schutzleiterpruefung", "Fehlerstrom-Schutzschalter als Zusatzmassnahme"}),
|
||||
TypicalCauses: []string{"Lose Schutzleiterklemme", "Korrosion an Erdungspunkten", "Vergessener Schutzleiteranschluss nach Wartung"},
|
||||
TypicalHarm: "Elektrischer Schlag bei Beruehrung des Maschinengehaeuses",
|
||||
RelevantLifecyclePhases: []string{"normal_operation", "maintenance", "installation"},
|
||||
RecommendedMeasuresDesign: []string{"Redundante Schutzleiteranschluesse", "Schutzleiter-Monitoring"},
|
||||
RecommendedMeasuresTechnical: []string{"RCD-Schutzschalter 30 mA", "Isolationsueberwachung"},
|
||||
RecommendedMeasuresInformation: []string{"Pruefplaketten an Schutzleiterpunkten", "Prueffrist 12 Monate"},
|
||||
SuggestedEvidence: []string{"Schutzleitermessung", "Pruefprotokoll DGUV V3"},
|
||||
RelatedKeywords: []string{"Schutzleiter", "Erdung", "Fehlerstrom"},
|
||||
IsBuiltin: true,
|
||||
TenantID: nil,
|
||||
CreatedAt: now,
|
||||
},
|
||||
{
|
||||
ID: hazardUUID("electrical_hazard", 10),
|
||||
Category: "electrical_hazard",
|
||||
SubCategory: "isolationsversagen",
|
||||
Name: "Isolationsversagen in Hochspannungsbereich",
|
||||
Description: "Alterung, Verschmutzung oder mechanische Beschaedigung der Isolierung in Hochspannungsbereichen kann zu Spannungsueberschlaegen und Koerperdurchstroemung fuehren.",
|
||||
DefaultSeverity: 5,
|
||||
DefaultProbability: 2,
|
||||
DefaultExposure: 2,
|
||||
DefaultAvoidance: 2,
|
||||
ApplicableComponentTypes: []string{"electrical"},
|
||||
RegulationReferences: []string{"Maschinenverordnung 2023/1230 Anhang I"},
|
||||
SuggestedMitigations: mustMarshalJSON([]string{"Isolationswiderstandsmessung", "Spannungsfeste Einhausung"}),
|
||||
TypicalCauses: []string{"Alterung der Isolierstoffe", "Mechanische Beschaedigung", "Verschmutzung und Feuchtigkeit"},
|
||||
TypicalHarm: "Toedlicher Stromschlag, Verbrennungen durch Spannungsueberschlag",
|
||||
RelevantLifecyclePhases: []string{"normal_operation", "maintenance"},
|
||||
RecommendedMeasuresDesign: []string{"Verstaerkte Isolierung in kritischen Bereichen", "Luftstrecken und Kriechstrecken einhalten"},
|
||||
RecommendedMeasuresTechnical: []string{"Isolationsueberwachungsgeraet", "Verriegelter Zugang zum Hochspannungsbereich"},
|
||||
RecommendedMeasuresInformation: []string{"Hochspannungswarnung", "Zutrittsregelung fuer Elektrofachkraefte"},
|
||||
SuggestedEvidence: []string{"Isolationsmessprotokoll", "Pruefbericht Hochspannungsbereich"},
|
||||
RelatedKeywords: []string{"Isolation", "Hochspannung", "Durchschlag"},
|
||||
IsBuiltin: true,
|
||||
TenantID: nil,
|
||||
CreatedAt: now,
|
||||
},
|
||||
// ====================================================================
|
||||
// Category: thermal_hazard (indices 5-8, 4 entries)
|
||||
// ====================================================================
|
||||
{
|
||||
ID: hazardUUID("thermal_hazard", 5),
|
||||
Category: "thermal_hazard",
|
||||
SubCategory: "kaeltekontakt",
|
||||
Name: "Kontakt mit kalten Oberflaechen (Kryotechnik)",
|
||||
Description: "In kryotechnischen Anlagen oder Kuehlsystemen koennen extrem kalte Oberflaechen bei Beruehrung Kaelteverbrennungen verursachen.",
|
||||
DefaultSeverity: 4,
|
||||
DefaultProbability: 2,
|
||||
DefaultExposure: 2,
|
||||
DefaultAvoidance: 3,
|
||||
ApplicableComponentTypes: []string{"mechanical", "other"},
|
||||
RegulationReferences: []string{"Maschinenverordnung 2023/1230 Anhang I"},
|
||||
SuggestedMitigations: mustMarshalJSON([]string{"Isolierung kalter Oberflaechen", "Kaelteschutzhandschuhe"}),
|
||||
TypicalCauses: []string{"Fehlende Isolierung an Kryoleitungen", "Beruehrung tiefgekuehlter Bauteile", "Defekte Kaelteisolierung"},
|
||||
TypicalHarm: "Kaelteverbrennungen an Haenden und Fingern",
|
||||
RelevantLifecyclePhases: []string{"normal_operation", "maintenance"},
|
||||
RecommendedMeasuresDesign: []string{"Isolierung aller kalten Oberflaechen im Zugriffsbereich", "Abstandshalter zu Kryoleitungen"},
|
||||
RecommendedMeasuresTechnical: []string{"Temperaturwarnung bei kritischen Oberflaechentemperaturen"},
|
||||
RecommendedMeasuresInformation: []string{"Warnhinweis Kaeltegefahr", "PSA-Pflicht Kaelteschutz"},
|
||||
SuggestedEvidence: []string{"Oberflaechentemperaturmessung", "Risikobeurteilung"},
|
||||
RelatedKeywords: []string{"Kryotechnik", "Kaelte", "Kaelteverbrennung"},
|
||||
IsBuiltin: true,
|
||||
TenantID: nil,
|
||||
CreatedAt: now,
|
||||
},
|
||||
{
|
||||
ID: hazardUUID("thermal_hazard", 6),
|
||||
Category: "thermal_hazard",
|
||||
SubCategory: "waermestrahlung",
|
||||
Name: "Waermestrahlung von Hochtemperaturprozessen",
|
||||
Description: "Oefen, Giessereianlagen oder Waermebehandlungsprozesse emittieren intensive Waermestrahlung, die auch ohne direkten Kontakt zu Verbrennungen fuehren kann.",
|
||||
DefaultSeverity: 4,
|
||||
DefaultProbability: 3,
|
||||
DefaultExposure: 3,
|
||||
DefaultAvoidance: 3,
|
||||
ApplicableComponentTypes: []string{"mechanical", "other"},
|
||||
RegulationReferences: []string{"Maschinenverordnung 2023/1230 Anhang I"},
|
||||
SuggestedMitigations: mustMarshalJSON([]string{"Waermeschutzschilder", "Hitzeschutzkleidung"}),
|
||||
TypicalCauses: []string{"Offene Ofentuer bei Beschickung", "Fehlende Abschirmung", "Langzeitexposition in der Naehe von Waermequellen"},
|
||||
TypicalHarm: "Hautverbrennungen durch Waermestrahlung, Hitzschlag",
|
||||
RelevantLifecyclePhases: []string{"normal_operation", "setup"},
|
||||
RecommendedMeasuresDesign: []string{"Waermedaemmung und Strahlungsschilde", "Automatische Beschickung statt manueller"},
|
||||
RecommendedMeasuresTechnical: []string{"Waermestrahlung-Sensor mit Warnung", "Luftschleier vor Ofenoeeffnungen"},
|
||||
RecommendedMeasuresInformation: []string{"Maximalaufenthaltsdauer festlegen", "Hitzeschutz-PSA vorschreiben"},
|
||||
SuggestedEvidence: []string{"Waermestrahlungsmessung am Arbeitsplatz", "Risikobeurteilung"},
|
||||
RelatedKeywords: []string{"Waermestrahlung", "Ofen", "Hitzeschutz"},
|
||||
IsBuiltin: true,
|
||||
TenantID: nil,
|
||||
CreatedAt: now,
|
||||
},
|
||||
{
|
||||
ID: hazardUUID("thermal_hazard", 7),
|
||||
Category: "thermal_hazard",
|
||||
SubCategory: "brandgefahr",
|
||||
Name: "Brandgefahr durch ueberhitzte Antriebe",
|
||||
Description: "Ueberlastete oder schlecht gekuehlte Elektromotoren und Antriebe koennen sich so stark erhitzen, dass umgebende Materialien entzuendet werden.",
|
||||
DefaultSeverity: 5,
|
||||
DefaultProbability: 2,
|
||||
DefaultExposure: 3,
|
||||
DefaultAvoidance: 3,
|
||||
ApplicableComponentTypes: []string{"actuator", "electrical"},
|
||||
RegulationReferences: []string{"Maschinenverordnung 2023/1230 Anhang I"},
|
||||
SuggestedMitigations: mustMarshalJSON([]string{"Temperatursensor am Motor", "Thermischer Motorschutz"}),
|
||||
TypicalCauses: []string{"Dauerbetrieb ueber Nennlast", "Blockierter Kuehlluftstrom", "Defektes Motorlager erhoecht Reibung"},
|
||||
TypicalHarm: "Brand mit Sachschaeden und Personengefaehrdung durch Rauchentwicklung",
|
||||
RelevantLifecyclePhases: []string{"normal_operation"},
|
||||
RecommendedMeasuresDesign: []string{"Thermische Motorschutzdimensionierung", "Brandschottung um Antriebsbereich"},
|
||||
RecommendedMeasuresTechnical: []string{"PTC-Temperaturfuehler im Motor", "Rauchmelder im Antriebsbereich"},
|
||||
RecommendedMeasuresInformation: []string{"Wartungsintervalle fuer Kuehlluftwege", "Brandschutzordnung"},
|
||||
SuggestedEvidence: []string{"Temperaturmessung unter Last", "Brandschutzkonzept"},
|
||||
RelatedKeywords: []string{"Motorueberhitzung", "Brand", "Thermischer Schutz"},
|
||||
IsBuiltin: true,
|
||||
TenantID: nil,
|
||||
CreatedAt: now,
|
||||
},
|
||||
{
|
||||
ID: hazardUUID("thermal_hazard", 8),
|
||||
Category: "thermal_hazard",
|
||||
SubCategory: "heisse_fluessigkeiten",
|
||||
Name: "Verbrennungsgefahr durch heisse Fluessigkeiten",
|
||||
Description: "Heisse Prozessfluessigkeiten, Kuehlmittel oder Dampf koennen bei Leckage oder beim Oeffnen von Verschluessen Verbruehungen verursachen.",
|
||||
DefaultSeverity: 4,
|
||||
DefaultProbability: 3,
|
||||
DefaultExposure: 3,
|
||||
DefaultAvoidance: 3,
|
||||
ApplicableComponentTypes: []string{"mechanical", "other"},
|
||||
RegulationReferences: []string{"Maschinenverordnung 2023/1230 Anhang I"},
|
||||
SuggestedMitigations: mustMarshalJSON([]string{"Druckentlastung vor dem Oeffnen", "Spritzschutz an Leitungsverbindungen"}),
|
||||
TypicalCauses: []string{"Oeffnen von Verschluessen unter Druck", "Schlauchbruch bei heissem Medium", "Spritzer beim Nachfuellen"},
|
||||
TypicalHarm: "Verbruehungen an Haut und Augen",
|
||||
RelevantLifecyclePhases: []string{"normal_operation", "maintenance"},
|
||||
RecommendedMeasuresDesign: []string{"Druckentlastungsventil vor Verschluss", "Isolierte Leitungsfuehrung"},
|
||||
RecommendedMeasuresTechnical: []string{"Temperaturanzeige an kritischen Punkten", "Auffangwannen unter Leitungsverbindungen"},
|
||||
RecommendedMeasuresInformation: []string{"Warnhinweis heisse Fluessigkeit", "Abkuehlprozedur in Betriebsanweisung"},
|
||||
SuggestedEvidence: []string{"Temperaturmessung am Austritt", "Risikobeurteilung"},
|
||||
RelatedKeywords: []string{"Verbruehung", "Heisse Fluessigkeit", "Dampf"},
|
||||
IsBuiltin: true,
|
||||
TenantID: nil,
|
||||
CreatedAt: now,
|
||||
},
|
||||
}
|
||||
}
|
||||
416
ai-compliance-sdk/internal/iace/hazard_library_iso12100_env.go
Normal file
416
ai-compliance-sdk/internal/iace/hazard_library_iso12100_env.go
Normal 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 Schulterhoehe"},
|
||||
TypicalHarm: "Bandscheibenvorfall, Rueckenverletzungen, Ueberanstrengung",
|
||||
RelevantLifecyclePhases: []string{"normal_operation", "maintenance", "setup"},
|
||||
RecommendedMeasuresDesign: []string{"Bauteile unter 15 kg fuer manuelles Handling", "Hebevorrichtungen integrieren"},
|
||||
RecommendedMeasuresTechnical: []string{"Kran oder Hebezeug am Arbeitsplatz", "Vakuumheber fuer Platten"},
|
||||
RecommendedMeasuresInformation: []string{"Hebebelastungstabelle aushangen", "Unterweisung in Hebetechnik"},
|
||||
SuggestedEvidence: []string{"Lastenhandhabungsbeurteilung", "Risikobeurteilung"},
|
||||
RelatedKeywords: []string{"Heben", "Lastenhandhabung", "Ueberanstrengung"},
|
||||
IsBuiltin: true,
|
||||
TenantID: nil,
|
||||
CreatedAt: now,
|
||||
},
|
||||
{
|
||||
ID: hazardUUID("ergonomic", 4),
|
||||
Category: "ergonomic",
|
||||
SubCategory: "verwechslung",
|
||||
Name: "Verwechslungsgefahr bei gleichartigen Bedienelementen",
|
||||
Description: "Baugleiche, nicht unterscheidbare Taster oder Schalter koennen verwechselt werden, was zu unbeabsichtigten und gefaehrlichen Maschinenaktionen fuehrt.",
|
||||
DefaultSeverity: 4,
|
||||
DefaultProbability: 3,
|
||||
DefaultExposure: 4,
|
||||
DefaultAvoidance: 4,
|
||||
ApplicableComponentTypes: []string{"controller", "sensor"},
|
||||
RegulationReferences: []string{"Maschinenverordnung 2023/1230 Anhang I"},
|
||||
SuggestedMitigations: mustMarshalJSON([]string{"Unterschiedliche Formen und Farben fuer verschiedene Funktionen", "Beschriftung in Klartext"}),
|
||||
TypicalCauses: []string{"Identische Tasterform fuer unterschiedliche Funktionen", "Fehlende Beschriftung", "Schlechte Beleuchtung am Bedienfeld"},
|
||||
TypicalHarm: "Unbeabsichtigte Maschinenaktionen mit Verletzungsgefahr",
|
||||
RelevantLifecyclePhases: []string{"normal_operation", "setup"},
|
||||
RecommendedMeasuresDesign: []string{"Form- und Farbcodierung nach IEC 60073", "Raeumliche Gruppierung nach Funktionsbereichen"},
|
||||
RecommendedMeasuresTechnical: []string{"Beleuchtetes Bedienfeld", "Haptisch unterscheidbare Taster"},
|
||||
RecommendedMeasuresInformation: []string{"Bedienfeldplan am Arbeitsplatz aushangen", "Einweisung neuer Bediener"},
|
||||
SuggestedEvidence: []string{"Usability-Bewertung", "Risikobeurteilung"},
|
||||
RelatedKeywords: []string{"Verwechslung", "Taster", "Bedienpanel"},
|
||||
IsBuiltin: true,
|
||||
TenantID: nil,
|
||||
CreatedAt: now,
|
||||
},
|
||||
{
|
||||
ID: hazardUUID("ergonomic", 5),
|
||||
Category: "ergonomic",
|
||||
SubCategory: "sichtbehinderung",
|
||||
Name: "Sichtbehinderung des Gefahrenbereichs vom Bedienplatz",
|
||||
Description: "Vom Bedienplatz aus ist der Gefahrenbereich nicht vollstaendig einsehbar, sodass der Bediener Personen im Gefahrenbereich nicht erkennen kann.",
|
||||
DefaultSeverity: 5,
|
||||
DefaultProbability: 3,
|
||||
DefaultExposure: 4,
|
||||
DefaultAvoidance: 2,
|
||||
ApplicableComponentTypes: []string{"controller", "sensor", "mechanical"},
|
||||
RegulationReferences: []string{"Maschinenverordnung 2023/1230 Anhang I"},
|
||||
SuggestedMitigations: mustMarshalJSON([]string{"Kameraueberwachung des Gefahrenbereichs", "Sicherheits-Laserscanner"}),
|
||||
TypicalCauses: []string{"Verdeckte Sicht durch Maschinenteile", "Bedienplatz zu weit vom Arbeitsbereich", "Fehlende Spiegel oder Kameras"},
|
||||
TypicalHarm: "Schwere Verletzungen von Personen im nicht einsehbaren Gefahrenbereich",
|
||||
RelevantLifecyclePhases: []string{"normal_operation", "setup"},
|
||||
RecommendedMeasuresDesign: []string{"Bedienplatz mit direkter Sicht auf Gefahrenbereich", "Transparente Schutzeinrichtungen"},
|
||||
RecommendedMeasuresTechnical: []string{"Kamerasystem mit Monitor am Bedienplatz", "Sicherheits-Laserscanner"},
|
||||
RecommendedMeasuresInformation: []string{"Hinweis auf toten Winkel", "Anlaufwarnung vor Maschinenbewegung"},
|
||||
SuggestedEvidence: []string{"Sichtfeldanalyse vom Bedienplatz", "Risikobeurteilung"},
|
||||
RelatedKeywords: []string{"Sichtfeld", "Toter Winkel", "Bedienplatz"},
|
||||
IsBuiltin: true,
|
||||
TenantID: nil,
|
||||
CreatedAt: now,
|
||||
},
|
||||
{
|
||||
ID: hazardUUID("ergonomic", 6),
|
||||
Category: "ergonomic",
|
||||
SubCategory: "griffgestaltung",
|
||||
Name: "Unergonomische Griffgestaltung von Handwerkzeugen",
|
||||
Description: "Schlecht geformte Griffe an handgefuehrten Werkzeugen und Hebeln fuehren zu Ermuedung, verminderter Griffkraft und erhoehtem Unfallrisiko.",
|
||||
DefaultSeverity: 3,
|
||||
DefaultProbability: 3,
|
||||
DefaultExposure: 4,
|
||||
DefaultAvoidance: 4,
|
||||
ApplicableComponentTypes: []string{"mechanical", "other"},
|
||||
RegulationReferences: []string{"Maschinenverordnung 2023/1230 Anhang I"},
|
||||
SuggestedMitigations: mustMarshalJSON([]string{"Ergonomische Griffformen nach EN 894", "Rutschfeste Griffoberflaechen"}),
|
||||
TypicalCauses: []string{"Zu duenner oder zu dicker Griff", "Glatte Griffoberflaeche", "Scharfe Kanten am Griff"},
|
||||
TypicalHarm: "Sehnenscheidenentzuendung, Abrutschen mit Folgeverletzung",
|
||||
RelevantLifecyclePhases: []string{"normal_operation", "maintenance"},
|
||||
RecommendedMeasuresDesign: []string{"Griffdurchmesser 30-45 mm", "Anatomisch geformte Griffe"},
|
||||
RecommendedMeasuresTechnical: []string{"Rutschfeste Beschichtung", "Handgelenkschlaufe an Handwerkzeugen"},
|
||||
RecommendedMeasuresInformation: []string{"Auswahl ergonomischer Werkzeuge dokumentieren", "Arbeitsmedizinische Beratung"},
|
||||
SuggestedEvidence: []string{"Ergonomische Werkzeugbewertung", "Risikobeurteilung"},
|
||||
RelatedKeywords: []string{"Griff", "Handwerkzeug", "Ergonomie"},
|
||||
IsBuiltin: true,
|
||||
TenantID: nil,
|
||||
CreatedAt: now,
|
||||
},
|
||||
{
|
||||
ID: hazardUUID("ergonomic", 7),
|
||||
Category: "ergonomic",
|
||||
SubCategory: "monotonie",
|
||||
Name: "Monotone Taetigkeit fuehrt zu Aufmerksamkeitsverlust",
|
||||
Description: "Langandauernde, sich wiederholende Taetigkeiten ohne Abwechslung vermindern die Aufmerksamkeit des Bedieners und erhoehen die Unfallgefahr.",
|
||||
DefaultSeverity: 4,
|
||||
DefaultProbability: 3,
|
||||
DefaultExposure: 4,
|
||||
DefaultAvoidance: 3,
|
||||
ApplicableComponentTypes: []string{"other"},
|
||||
RegulationReferences: []string{"Maschinenverordnung 2023/1230 Anhang I"},
|
||||
SuggestedMitigations: mustMarshalJSON([]string{"Aufgabenwechsel organisieren", "Regelmaessige Pausen einplanen"}),
|
||||
TypicalCauses: []string{"Gleichfoermige Wiederholtaetigkeit", "Fehlende Pausenregelung", "Mangelnde Aufgabenvielfalt"},
|
||||
TypicalHarm: "Unfaelle durch Unaufmerksamkeit, verspaetete Reaktion auf Gefahren",
|
||||
RelevantLifecyclePhases: []string{"normal_operation"},
|
||||
RecommendedMeasuresDesign: []string{"Automatisierung monotoner Teilaufgaben", "Arbeitsplatzrotation ermoeglichen"},
|
||||
RecommendedMeasuresTechnical: []string{"Aufmerksamkeitserkennung", "Regelmaessige Warnmeldungen"},
|
||||
RecommendedMeasuresInformation: []string{"Pausenplan erstellen", "Schulung zur Ermuedungserkennung"},
|
||||
SuggestedEvidence: []string{"Arbeitsplatzanalyse", "Gefaehrdungsbeurteilung psychische Belastung"},
|
||||
RelatedKeywords: []string{"Monotonie", "Aufmerksamkeit", "Ermuedung"},
|
||||
IsBuiltin: true,
|
||||
TenantID: nil,
|
||||
CreatedAt: now,
|
||||
},
|
||||
{
|
||||
ID: hazardUUID("ergonomic", 8),
|
||||
Category: "ergonomic",
|
||||
SubCategory: "wartungszugang",
|
||||
Name: "Ungenuegender Zugang fuer Wartungsarbeiten",
|
||||
Description: "Enge oder schwer zugaengliche Wartungsbereiche zwingen das Personal zu gefaehrlichen Koerperhaltungen und erhoehen das Risiko von Verletzungen.",
|
||||
DefaultSeverity: 3,
|
||||
DefaultProbability: 3,
|
||||
DefaultExposure: 3,
|
||||
DefaultAvoidance: 3,
|
||||
ApplicableComponentTypes: []string{"mechanical", "other"},
|
||||
RegulationReferences: []string{"Maschinenverordnung 2023/1230 Anhang I"},
|
||||
SuggestedMitigations: mustMarshalJSON([]string{"Wartungsoeffnungen ausreichend dimensionieren", "Wartungsbuehnen und Podeste vorsehen"}),
|
||||
TypicalCauses: []string{"Zu kleine Wartungsoeffnungen", "Fehlende Arbeitsbuehnen in der Hoehe", "Wartungspunkte hinter schwer zu demontierenden Verkleidungen"},
|
||||
TypicalHarm: "Quetschungen in engen Raeumen, Absturz von erhoehten Wartungspositionen",
|
||||
RelevantLifecyclePhases: []string{"maintenance", "fault_finding"},
|
||||
RecommendedMeasuresDesign: []string{"Mindestmasse fuer Zugangsoeeffnungen nach ISO 14122", "Wartungspunkte leicht zugaenglich anordnen"},
|
||||
RecommendedMeasuresTechnical: []string{"Feste Buehnen und Leitern mit Absturzsicherung", "Schnellverschluesse statt Schraubverbindungen an Wartungsklappen"},
|
||||
RecommendedMeasuresInformation: []string{"Wartungszugangsskizze in Betriebsanleitung", "Hinweis auf PSA-Pflicht bei Arbeiten in der Hoehe"},
|
||||
SuggestedEvidence: []string{"Zugaenglichkeitspruefung", "Risikobeurteilung"},
|
||||
RelatedKeywords: []string{"Wartungszugang", "Zugaenglichkeit", "Wartungsbuehne"},
|
||||
IsBuiltin: true,
|
||||
TenantID: nil,
|
||||
CreatedAt: now,
|
||||
},
|
||||
// ====================================================================
|
||||
// Category: material_environmental (indices 1-8, 8 entries)
|
||||
// ====================================================================
|
||||
{
|
||||
ID: hazardUUID("material_environmental", 1),
|
||||
Category: "material_environmental",
|
||||
SubCategory: "staubexplosion",
|
||||
Name: "Staubexplosion durch Feinpartikel",
|
||||
Description: "Feine brennbare Staube koennen in Verbindung mit einer Zuendquelle explosionsartig reagieren und schwere Zerstoerungen verursachen.",
|
||||
DefaultSeverity: 5,
|
||||
DefaultProbability: 2,
|
||||
DefaultExposure: 3,
|
||||
DefaultAvoidance: 2,
|
||||
ApplicableComponentTypes: []string{"mechanical", "other"},
|
||||
RegulationReferences: []string{"Maschinenverordnung 2023/1230 Anhang I"},
|
||||
SuggestedMitigations: mustMarshalJSON([]string{"ATEX-konforme Ausfuehrung", "Absauganlage mit Funkenerkennung"}),
|
||||
TypicalCauses: []string{"Unzureichende Absaugung", "Staubansammlung in Hohlraeumen", "Elektrische oder mechanische Zuendquelle"},
|
||||
TypicalHarm: "Explosion mit toedlichen Verletzungen, schwere Verbrennungen, Gebaeudezerstoerung",
|
||||
RelevantLifecyclePhases: []string{"normal_operation", "cleaning"},
|
||||
RecommendedMeasuresDesign: []string{"ATEX-konforme Konstruktion", "Druckentlastungsflaechen vorsehen"},
|
||||
RecommendedMeasuresTechnical: []string{"Absaugung mit Funkenloeschanlage", "Explosionsunterdrueckung"},
|
||||
RecommendedMeasuresInformation: []string{"Ex-Zoneneinteilung kennzeichnen", "Reinigungsplan fuer Staubablagerungen"},
|
||||
SuggestedEvidence: []string{"Explosionsschutz-Dokument", "ATEX-Konformitaetserklaerung"},
|
||||
RelatedKeywords: []string{"Staubexplosion", "ATEX", "Feinstaub"},
|
||||
IsBuiltin: true,
|
||||
TenantID: nil,
|
||||
CreatedAt: now,
|
||||
},
|
||||
{
|
||||
ID: hazardUUID("material_environmental", 2),
|
||||
Category: "material_environmental",
|
||||
SubCategory: "rauch_gas",
|
||||
Name: "Rauch- und Gasfreisetzung bei Laserschneiden",
|
||||
Description: "Beim Laserschneiden entstehen gesundheitsschaedliche Rauche und Gase aus dem verdampften Material, die die Atemwege schaedigen.",
|
||||
DefaultSeverity: 4,
|
||||
DefaultProbability: 4,
|
||||
DefaultExposure: 3,
|
||||
DefaultAvoidance: 3,
|
||||
ApplicableComponentTypes: []string{"mechanical", "other"},
|
||||
RegulationReferences: []string{"Maschinenverordnung 2023/1230 Anhang I"},
|
||||
SuggestedMitigations: mustMarshalJSON([]string{"Absaugung direkt am Bearbeitungspunkt", "Filteranlage mit Aktivkohlestufe"}),
|
||||
TypicalCauses: []string{"Fehlende Absaugung", "Verarbeitung kunststoffbeschichteter Materialien", "Undichte Maschineneinhausung"},
|
||||
TypicalHarm: "Atemwegserkrankungen, Reizung von Augen und Schleimhaeuten",
|
||||
RelevantLifecyclePhases: []string{"normal_operation"},
|
||||
RecommendedMeasuresDesign: []string{"Geschlossener Bearbeitungsraum mit Absaugung", "Materialauswahl ohne toxische Beschichtungen"},
|
||||
RecommendedMeasuresTechnical: []string{"Punktabsaugung mit HEPA-Filter", "Raumluft-Monitoring"},
|
||||
RecommendedMeasuresInformation: []string{"Materialfreigabeliste pflegen", "Atemschutz-PSA bei Sondermaterialien"},
|
||||
SuggestedEvidence: []string{"Arbeitsplatzmessung Gefahrstoffe", "Risikobeurteilung"},
|
||||
RelatedKeywords: []string{"Laserschneiden", "Rauch", "Gefahrstoff"},
|
||||
IsBuiltin: true,
|
||||
TenantID: nil,
|
||||
CreatedAt: now,
|
||||
},
|
||||
{
|
||||
ID: hazardUUID("material_environmental", 3),
|
||||
Category: "material_environmental",
|
||||
SubCategory: "kuehlschmierstoff",
|
||||
Name: "Daempfe aus Kuehlschmierstoffen",
|
||||
Description: "Beim Einsatz von Kuehlschmierstoffen entstehen Aerosole und Daempfe, die bei Langzeitexposition zu Haut- und Atemwegserkrankungen fuehren.",
|
||||
DefaultSeverity: 3,
|
||||
DefaultProbability: 3,
|
||||
DefaultExposure: 4,
|
||||
DefaultAvoidance: 3,
|
||||
ApplicableComponentTypes: []string{"mechanical", "other"},
|
||||
RegulationReferences: []string{"Maschinenverordnung 2023/1230 Anhang I"},
|
||||
SuggestedMitigations: mustMarshalJSON([]string{"Oelunebelabscheider an der Maschine", "Hautschutzplan fuer Bediener"}),
|
||||
TypicalCauses: []string{"Offene Bearbeitungszonen ohne Abschirmung", "Ueberaltertes Kuehlschmiermittel", "Zu hoher Kuehlmitteldurchsatz"},
|
||||
TypicalHarm: "Oelakne, Atemwegssensibilisierung, allergische Hautreaktionen",
|
||||
RelevantLifecyclePhases: []string{"normal_operation"},
|
||||
RecommendedMeasuresDesign: []string{"Geschlossener Bearbeitungsraum", "Minimalmengenschmierung statt Volumenoelstrom"},
|
||||
RecommendedMeasuresTechnical: []string{"Oelunebelabscheider", "KSS-Konzentrations- und pH-Ueberwachung"},
|
||||
RecommendedMeasuresInformation: []string{"Hautschutz- und Hygieneplan", "KSS-Pflegeanweisung"},
|
||||
SuggestedEvidence: []string{"Arbeitsplatz-Gefahrstoffmessung", "Hautschutzplan"},
|
||||
RelatedKeywords: []string{"Kuehlschmierstoff", "KSS", "Aerosol"},
|
||||
IsBuiltin: true,
|
||||
TenantID: nil,
|
||||
CreatedAt: now,
|
||||
},
|
||||
{
|
||||
ID: hazardUUID("material_environmental", 4),
|
||||
Category: "material_environmental",
|
||||
SubCategory: "prozessmedien",
|
||||
Name: "Chemische Exposition durch Prozessmedien",
|
||||
Description: "Chemisch aggressive Prozessmedien wie Saeuren, Laugen oder Loesemittel koennen bei Hautkontakt oder Einatmen schwere Gesundheitsschaeden verursachen.",
|
||||
DefaultSeverity: 4,
|
||||
DefaultProbability: 3,
|
||||
DefaultExposure: 3,
|
||||
DefaultAvoidance: 3,
|
||||
ApplicableComponentTypes: []string{"other", "mechanical"},
|
||||
RegulationReferences: []string{"Maschinenverordnung 2023/1230 Anhang I"},
|
||||
SuggestedMitigations: mustMarshalJSON([]string{"Geschlossene Mediumfuehrung", "Chemikalienschutz-PSA"}),
|
||||
TypicalCauses: []string{"Offene Behaelter mit Gefahrstoffen", "Spritzer beim Nachfuellen", "Leckage an Dichtungen"},
|
||||
TypicalHarm: "Veraetzungen, Atemwegsschaedigung, Vergiftung",
|
||||
RelevantLifecyclePhases: []string{"normal_operation", "maintenance", "cleaning"},
|
||||
RecommendedMeasuresDesign: []string{"Geschlossene Kreislaeufe", "Korrosionsbestaendige Materialien"},
|
||||
RecommendedMeasuresTechnical: []string{"Notdusche und Augenspueler", "Gaswarnanlage"},
|
||||
RecommendedMeasuresInformation: []string{"Sicherheitsdatenblaetter am Arbeitsplatz", "Gefahrstoffunterweisung"},
|
||||
SuggestedEvidence: []string{"Gefahrstoffverzeichnis", "Arbeitsplatzmessung"},
|
||||
RelatedKeywords: []string{"Gefahrstoff", "Chemikalie", "Prozessmedium"},
|
||||
IsBuiltin: true,
|
||||
TenantID: nil,
|
||||
CreatedAt: now,
|
||||
},
|
||||
{
|
||||
ID: hazardUUID("material_environmental", 5),
|
||||
Category: "material_environmental",
|
||||
SubCategory: "dichtungsversagen",
|
||||
Name: "Leckage von Gefahrstoffen bei Dichtungsversagen",
|
||||
Description: "Versagende Dichtungen an Rohrleitungen oder Behaeltern setzen Gefahrstoffe frei und gefaehrden Personal und Umwelt.",
|
||||
DefaultSeverity: 4,
|
||||
DefaultProbability: 3,
|
||||
DefaultExposure: 3,
|
||||
DefaultAvoidance: 3,
|
||||
ApplicableComponentTypes: []string{"mechanical", "other"},
|
||||
RegulationReferences: []string{"Maschinenverordnung 2023/1230 Anhang I"},
|
||||
SuggestedMitigations: mustMarshalJSON([]string{"Doppelwandige Behaelter mit Leckagedetektion", "Auffangwannen unter allen Gefahrstoffbereichen"}),
|
||||
TypicalCauses: []string{"Alterung der Dichtungsmaterialien", "Chemische Unvertraeglichkeit", "Thermische Ueberbelastung der Dichtung"},
|
||||
TypicalHarm: "Gefahrstofffreisetzung mit Vergiftung, Hautschaedigung, Umweltschaden",
|
||||
RelevantLifecyclePhases: []string{"normal_operation", "maintenance"},
|
||||
RecommendedMeasuresDesign: []string{"Doppelwandige Leitungen", "Chemisch bestaendige Dichtungswerkstoffe"},
|
||||
RecommendedMeasuresTechnical: []string{"Leckagesensoren", "Auffangwannen mit Fassungsvermoegen 110%"},
|
||||
RecommendedMeasuresInformation: []string{"Dichtungswechselintervalle festlegen", "Notfallplan Gefahrstoffaustritt"},
|
||||
SuggestedEvidence: []string{"Dichtheitspruefprotokoll", "Gefahrstoff-Notfallplan"},
|
||||
RelatedKeywords: []string{"Leckage", "Dichtung", "Gefahrstoff"},
|
||||
IsBuiltin: true,
|
||||
TenantID: nil,
|
||||
CreatedAt: now,
|
||||
},
|
||||
{
|
||||
ID: hazardUUID("material_environmental", 6),
|
||||
Category: "material_environmental",
|
||||
SubCategory: "biologische_kontamination",
|
||||
Name: "Biologische Kontamination in Lebensmittelmaschinen",
|
||||
Description: "In Maschinen der Lebensmittelindustrie koennen sich Mikroorganismen in schwer zu reinigenden Bereichen ansiedeln und das Produkt kontaminieren.",
|
||||
DefaultSeverity: 4,
|
||||
DefaultProbability: 3,
|
||||
DefaultExposure: 3,
|
||||
DefaultAvoidance: 3,
|
||||
ApplicableComponentTypes: []string{"mechanical", "other"},
|
||||
RegulationReferences: []string{"Maschinenverordnung 2023/1230 Anhang I"},
|
||||
SuggestedMitigations: mustMarshalJSON([]string{"Hygienic-Design-Konstruktion", "CIP-Reinigungssystem"}),
|
||||
TypicalCauses: []string{"Totraeume und Hinterschneidungen in der Konstruktion", "Unzureichende Reinigung", "Poroese Oberflaechen"},
|
||||
TypicalHarm: "Lebensmittelkontamination mit Gesundheitsgefaehrdung der Verbraucher",
|
||||
RelevantLifecyclePhases: []string{"normal_operation", "cleaning", "maintenance"},
|
||||
RecommendedMeasuresDesign: []string{"Hygienic Design nach EHEDG-Richtlinien", "Selbstentleerende Konstruktion"},
|
||||
RecommendedMeasuresTechnical: []string{"CIP-Reinigungsanlage", "Oberflaechenguete Ra kleiner 0.8 Mikrometer"},
|
||||
RecommendedMeasuresInformation: []string{"Reinigungs- und Desinfektionsplan", "Hygieneschulung des Personals"},
|
||||
SuggestedEvidence: []string{"EHEDG-Zertifikat", "Mikrobiologische Abklatschproben"},
|
||||
RelatedKeywords: []string{"Hygiene", "Kontamination", "Lebensmittelsicherheit"},
|
||||
IsBuiltin: true,
|
||||
TenantID: nil,
|
||||
CreatedAt: now,
|
||||
},
|
||||
{
|
||||
ID: hazardUUID("material_environmental", 7),
|
||||
Category: "material_environmental",
|
||||
SubCategory: "elektrostatik",
|
||||
Name: "Statische Aufladung in staubhaltiger Umgebung",
|
||||
Description: "In staubhaltiger oder explosionsfaehiger Atmosphaere kann eine elektrostatische Entladung als Zuendquelle wirken und eine Explosion oder einen Brand ausloesen.",
|
||||
DefaultSeverity: 5,
|
||||
DefaultProbability: 2,
|
||||
DefaultExposure: 3,
|
||||
DefaultAvoidance: 3,
|
||||
ApplicableComponentTypes: []string{"mechanical", "electrical", "other"},
|
||||
RegulationReferences: []string{"Maschinenverordnung 2023/1230 Anhang I"},
|
||||
SuggestedMitigations: mustMarshalJSON([]string{"Erdung aller leitfaehigen Teile", "Ableitfaehige Bodenbeschichtung"}),
|
||||
TypicalCauses: []string{"Nicht geerdete Maschinenteile", "Reibung von Kunststoffbaendern", "Niedrige Luftfeuchtigkeit"},
|
||||
TypicalHarm: "Zuendung explosionsfaehiger Atmosphaere, Explosion oder Brand",
|
||||
RelevantLifecyclePhases: []string{"normal_operation"},
|
||||
RecommendedMeasuresDesign: []string{"Leitfaehige Materialien verwenden", "Erdungskonzept fuer alle Komponenten"},
|
||||
RecommendedMeasuresTechnical: []string{"Ionisatoren zur Ladungsneutralisation", "Erdungsueberwachung"},
|
||||
RecommendedMeasuresInformation: []string{"ESD-Hinweisschilder", "Schulung zur elektrostatischen Gefaehrdung"},
|
||||
SuggestedEvidence: []string{"Erdungsmessung", "Explosionsschutz-Dokument"},
|
||||
RelatedKeywords: []string{"Elektrostatik", "ESD", "Zuendquelle"},
|
||||
IsBuiltin: true,
|
||||
TenantID: nil,
|
||||
CreatedAt: now,
|
||||
},
|
||||
{
|
||||
ID: hazardUUID("material_environmental", 8),
|
||||
Category: "material_environmental",
|
||||
SubCategory: "uv_strahlung",
|
||||
Name: "UV-Strahlung bei Schweiss- oder Haerteprozessen",
|
||||
Description: "Schweissvorgaenge und UV-Haerteprozesse emittieren ultraviolette Strahlung, die Augen und Haut schwer schaedigen kann.",
|
||||
DefaultSeverity: 4,
|
||||
DefaultProbability: 3,
|
||||
DefaultExposure: 3,
|
||||
DefaultAvoidance: 3,
|
||||
ApplicableComponentTypes: []string{"mechanical", "electrical", "other"},
|
||||
RegulationReferences: []string{"Maschinenverordnung 2023/1230 Anhang I"},
|
||||
SuggestedMitigations: mustMarshalJSON([]string{"Abschirmung des UV-Bereichs", "Schweissschutzschirm und Schutzkleidung"}),
|
||||
TypicalCauses: []string{"Fehlende Abschirmung der UV-Quelle", "Reflexion an glaenzenden Oberflaechen", "Aufenthalt im Strahlungsbereich ohne Schutz"},
|
||||
TypicalHarm: "Verblitzen der Augen, Hautverbrennungen, erhoehtes Hautkrebsrisiko bei Langzeitexposition",
|
||||
RelevantLifecyclePhases: []string{"normal_operation", "maintenance"},
|
||||
RecommendedMeasuresDesign: []string{"Vollstaendige Einhausung der UV-Quelle", "UV-absorbierende Schutzscheiben"},
|
||||
RecommendedMeasuresTechnical: []string{"Schweissvorhaenge um den Arbeitsbereich", "UV-Sensor mit Abschaltung"},
|
||||
RecommendedMeasuresInformation: []string{"Warnhinweis UV-Strahlung", "PSA-Pflicht: Schweissschutzhelm und Schutzkleidung"},
|
||||
SuggestedEvidence: []string{"UV-Strahlungsmessung", "Risikobeurteilung"},
|
||||
RelatedKeywords: []string{"UV-Strahlung", "Schweissen", "Strahlenschutz"},
|
||||
IsBuiltin: true,
|
||||
TenantID: nil,
|
||||
CreatedAt: now,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,362 @@
|
||||
package iace
|
||||
|
||||
import "time"
|
||||
|
||||
// builtinHazardsISO12100Mechanical returns ISO 12100 mechanical hazard
|
||||
// entries (indices 7-20) per Maschinenverordnung 2023/1230 and ISO 12100.
|
||||
func builtinHazardsISO12100Mechanical() []HazardLibraryEntry {
|
||||
now := time.Now()
|
||||
return []HazardLibraryEntry{
|
||||
// ====================================================================
|
||||
{
|
||||
ID: hazardUUID("mechanical_hazard", 7),
|
||||
Category: "mechanical_hazard",
|
||||
SubCategory: "quetschgefahr",
|
||||
Name: "Quetschgefahr durch gegenlaeufige Walzen",
|
||||
Description: "Zwischen gegenlaeufig rotierenden Walzen entsteht ein Einzugspunkt, an dem Koerperteile oder Kleidung eingezogen und gequetscht werden koennen.",
|
||||
DefaultSeverity: 5,
|
||||
DefaultProbability: 3,
|
||||
DefaultExposure: 3,
|
||||
DefaultAvoidance: 3,
|
||||
ApplicableComponentTypes: []string{"mechanical", "actuator"},
|
||||
RegulationReferences: []string{"Maschinenverordnung 2023/1230 Anhang I"},
|
||||
SuggestedMitigations: mustMarshalJSON([]string{"Feststehende trennende Schutzeinrichtung am Walzeneinlauf", "Zweihandbedienung bei manueller Beschickung"}),
|
||||
TypicalCauses: []string{"Fehlende Schutzabdeckung am Einzugspunkt", "Manuelle Materialzufuehrung ohne Hilfsmittel", "Wartung bei laufender Maschine"},
|
||||
TypicalHarm: "Quetschverletzungen an Fingern, Haenden oder Armen bis hin zu Amputationen",
|
||||
RelevantLifecyclePhases: []string{"normal_operation", "maintenance", "setup"},
|
||||
RecommendedMeasuresDesign: []string{"Mindestabstand zwischen Walzen groesser als 25 mm oder kleiner als 5 mm", "Einzugspunkt ausserhalb der Reichweite positionieren"},
|
||||
RecommendedMeasuresTechnical: []string{"Schutzgitter mit Sicherheitsverriegelung", "Lichtschranke vor dem Einzugsbereich"},
|
||||
RecommendedMeasuresInformation: []string{"Warnschilder am Einzugspunkt", "Betriebsanweisung zur sicheren Beschickung"},
|
||||
SuggestedEvidence: []string{"Pruefbericht der Schutzeinrichtung", "Risikobeurteilung nach ISO 12100"},
|
||||
RelatedKeywords: []string{"Walzen", "Einzugspunkt", "Quetschstelle"},
|
||||
IsBuiltin: true,
|
||||
TenantID: nil,
|
||||
CreatedAt: now,
|
||||
},
|
||||
{
|
||||
ID: hazardUUID("mechanical_hazard", 8),
|
||||
Category: "mechanical_hazard",
|
||||
SubCategory: "schergefahr",
|
||||
Name: "Schergefahr an beweglichen Maschinenteilen",
|
||||
Description: "Durch gegeneinander bewegte Maschinenteile entstehen Scherstellen, die zu schweren Schnitt- und Trennverletzungen fuehren koennen.",
|
||||
DefaultSeverity: 5,
|
||||
DefaultProbability: 3,
|
||||
DefaultExposure: 3,
|
||||
DefaultAvoidance: 3,
|
||||
ApplicableComponentTypes: []string{"mechanical", "actuator"},
|
||||
RegulationReferences: []string{"Maschinenverordnung 2023/1230 Anhang I"},
|
||||
SuggestedMitigations: mustMarshalJSON([]string{"Trennende Schutzeinrichtung an der Scherstelle", "Sicherheitsabstand nach ISO 13857"}),
|
||||
TypicalCauses: []string{"Unzureichender Sicherheitsabstand", "Fehlende Schutzverkleidung", "Eingriff waehrend des Betriebs"},
|
||||
TypicalHarm: "Schnitt- und Trennverletzungen an Fingern und Haenden",
|
||||
RelevantLifecyclePhases: []string{"normal_operation", "maintenance"},
|
||||
RecommendedMeasuresDesign: []string{"Sicherheitsabstaende nach ISO 13857 einhalten", "Scherstellen konstruktiv vermeiden"},
|
||||
RecommendedMeasuresTechnical: []string{"Verriegelte Schutzhauben", "Not-Halt in unmittelbarer Naehe"},
|
||||
RecommendedMeasuresInformation: []string{"Gefahrenhinweis an Scherstellen", "Schulung der Bediener"},
|
||||
SuggestedEvidence: []string{"Abstandsmessung gemaess ISO 13857", "Risikobeurteilung"},
|
||||
RelatedKeywords: []string{"Scherstelle", "Gegenlaeufig", "Schneidgefahr"},
|
||||
IsBuiltin: true,
|
||||
TenantID: nil,
|
||||
CreatedAt: now,
|
||||
},
|
||||
{
|
||||
ID: hazardUUID("mechanical_hazard", 9),
|
||||
Category: "mechanical_hazard",
|
||||
SubCategory: "schneidgefahr",
|
||||
Name: "Schneidgefahr durch rotierende Werkzeuge",
|
||||
Description: "Rotierende Schneidwerkzeuge wie Fraeser, Saegeblaetter oder Messer koennen bei Kontakt schwere Schnittverletzungen verursachen.",
|
||||
DefaultSeverity: 5,
|
||||
DefaultProbability: 3,
|
||||
DefaultExposure: 3,
|
||||
DefaultAvoidance: 2,
|
||||
ApplicableComponentTypes: []string{"mechanical"},
|
||||
RegulationReferences: []string{"Maschinenverordnung 2023/1230 Anhang I"},
|
||||
SuggestedMitigations: mustMarshalJSON([]string{"Vollstaendige Einhausung des Werkzeugs", "Automatische Werkzeugbremse bei Schutztueroeffnung"}),
|
||||
TypicalCauses: []string{"Offene Schutzhaube waehrend des Betriebs", "Nachlauf des Werkzeugs nach Abschaltung", "Werkzeugbruch"},
|
||||
TypicalHarm: "Tiefe Schnittwunden bis hin zu Gliedmassentrennung",
|
||||
RelevantLifecyclePhases: []string{"normal_operation", "setup", "maintenance"},
|
||||
RecommendedMeasuresDesign: []string{"Vollstaendige Einhausung mit Verriegelung", "Werkzeugbremse mit kurzer Nachlaufzeit"},
|
||||
RecommendedMeasuresTechnical: []string{"Verriegelte Schutzhaube mit Zuhaltung", "Drehzahlueberwachung"},
|
||||
RecommendedMeasuresInformation: []string{"Warnhinweis zur Nachlaufzeit", "Betriebsanweisung zum Werkzeugwechsel"},
|
||||
SuggestedEvidence: []string{"Nachlaufzeitmessung", "Pruefbericht Schutzeinrichtung"},
|
||||
RelatedKeywords: []string{"Fraeser", "Saegeblatt", "Schneidwerkzeug"},
|
||||
IsBuiltin: true,
|
||||
TenantID: nil,
|
||||
CreatedAt: now,
|
||||
},
|
||||
{
|
||||
ID: hazardUUID("mechanical_hazard", 10),
|
||||
Category: "mechanical_hazard",
|
||||
SubCategory: "einzugsgefahr",
|
||||
Name: "Einzugsgefahr durch Foerderbaender",
|
||||
Description: "An Umlenkrollen und Antriebstrommeln von Foerderbaendern bestehen Einzugsstellen, die Koerperteile oder Kleidung erfassen koennen.",
|
||||
DefaultSeverity: 4,
|
||||
DefaultProbability: 3,
|
||||
DefaultExposure: 4,
|
||||
DefaultAvoidance: 3,
|
||||
ApplicableComponentTypes: []string{"mechanical", "actuator"},
|
||||
RegulationReferences: []string{"Maschinenverordnung 2023/1230 Anhang I"},
|
||||
SuggestedMitigations: mustMarshalJSON([]string{"Schutzverkleidung an Umlenkrollen", "Not-Halt-Reissleine entlang des Foerderbands"}),
|
||||
TypicalCauses: []string{"Fehlende Abdeckung an Umlenkpunkten", "Reinigung bei laufendem Band", "Lose Kleidung des Personals"},
|
||||
TypicalHarm: "Einzugsverletzungen an Armen und Haenden, Quetschungen",
|
||||
RelevantLifecyclePhases: []string{"normal_operation", "cleaning", "maintenance"},
|
||||
RecommendedMeasuresDesign: []string{"Umlenkrollen mit Schutzverkleidung", "Unterflur-Foerderung wo moeglich"},
|
||||
RecommendedMeasuresTechnical: []string{"Not-Halt-Reissleine", "Bandschieflauf-Erkennung"},
|
||||
RecommendedMeasuresInformation: []string{"Kleidervorschrift fuer Bedienpersonal", "Sicherheitsunterweisung"},
|
||||
SuggestedEvidence: []string{"Pruefbericht der Schutzeinrichtungen", "Risikobeurteilung"},
|
||||
RelatedKeywords: []string{"Foerderband", "Umlenkrolle", "Einzugsstelle"},
|
||||
IsBuiltin: true,
|
||||
TenantID: nil,
|
||||
CreatedAt: now,
|
||||
},
|
||||
{
|
||||
ID: hazardUUID("mechanical_hazard", 11),
|
||||
Category: "mechanical_hazard",
|
||||
SubCategory: "erfassungsgefahr",
|
||||
Name: "Erfassungsgefahr durch rotierende Wellen",
|
||||
Description: "Freiliegende rotierende Wellen, Kupplungen oder Zapfen koennen Kleidung oder Haare erfassen und Personen in die Drehbewegung hineinziehen.",
|
||||
DefaultSeverity: 5,
|
||||
DefaultProbability: 3,
|
||||
DefaultExposure: 3,
|
||||
DefaultAvoidance: 2,
|
||||
ApplicableComponentTypes: []string{"mechanical", "actuator"},
|
||||
RegulationReferences: []string{"Maschinenverordnung 2023/1230 Anhang I"},
|
||||
SuggestedMitigations: mustMarshalJSON([]string{"Vollstaendige Verkleidung rotierender Wellen", "Drehmomentbegrenzung"}),
|
||||
TypicalCauses: []string{"Fehlende Wellenabdeckung", "Lose Kleidungsstuecke", "Wartung bei laufender Welle"},
|
||||
TypicalHarm: "Erfassungsverletzungen mit Knochenbruechen, Skalpierungen oder toedlichem Ausgang",
|
||||
RelevantLifecyclePhases: []string{"normal_operation", "maintenance"},
|
||||
RecommendedMeasuresDesign: []string{"Wellen vollstaendig einhausen", "Kupplungen mit Schutzhuelsen"},
|
||||
RecommendedMeasuresTechnical: []string{"Verriegelte Schutzabdeckung", "Stillstandsueberwachung fuer Wartungszugang"},
|
||||
RecommendedMeasuresInformation: []string{"Kleiderordnung ohne lose Teile", "Warnschilder an Wellenabdeckungen"},
|
||||
SuggestedEvidence: []string{"Inspektionsbericht Wellenabdeckungen", "Risikobeurteilung"},
|
||||
RelatedKeywords: []string{"Welle", "Kupplung", "Erfassung"},
|
||||
IsBuiltin: true,
|
||||
TenantID: nil,
|
||||
CreatedAt: now,
|
||||
},
|
||||
{
|
||||
ID: hazardUUID("mechanical_hazard", 12),
|
||||
Category: "mechanical_hazard",
|
||||
SubCategory: "stossgefahr",
|
||||
Name: "Stossgefahr durch pneumatische/hydraulische Zylinder",
|
||||
Description: "Schnell ausfahrende Pneumatik- oder Hydraulikzylinder koennen Personen stossen oder einklemmen, insbesondere bei unerwartetem Anlauf.",
|
||||
DefaultSeverity: 4,
|
||||
DefaultProbability: 3,
|
||||
DefaultExposure: 3,
|
||||
DefaultAvoidance: 3,
|
||||
ApplicableComponentTypes: []string{"actuator", "mechanical"},
|
||||
RegulationReferences: []string{"Maschinenverordnung 2023/1230 Anhang I"},
|
||||
SuggestedMitigations: mustMarshalJSON([]string{"Geschwindigkeitsbegrenzung durch Drosselventile", "Schutzeinrichtung im Bewegungsbereich"}),
|
||||
TypicalCauses: []string{"Fehlende Endlagendaempfung", "Unerwarteter Druckaufbau", "Aufenthalt im Bewegungsbereich"},
|
||||
TypicalHarm: "Prellungen, Knochenbrueche, Einklemmverletzungen",
|
||||
RelevantLifecyclePhases: []string{"normal_operation", "setup", "maintenance"},
|
||||
RecommendedMeasuresDesign: []string{"Endlagendaempfung vorsehen", "Zylindergeschwindigkeit begrenzen"},
|
||||
RecommendedMeasuresTechnical: []string{"Lichtvorhang im Bewegungsbereich", "Druckspeicher-Entlastungsventil"},
|
||||
RecommendedMeasuresInformation: []string{"Kennzeichnung des Bewegungsbereichs", "Betriebsanweisung"},
|
||||
SuggestedEvidence: []string{"Geschwindigkeitsmessung", "Risikobeurteilung"},
|
||||
RelatedKeywords: []string{"Zylinder", "Pneumatik", "Stossgefahr"},
|
||||
IsBuiltin: true,
|
||||
TenantID: nil,
|
||||
CreatedAt: now,
|
||||
},
|
||||
{
|
||||
ID: hazardUUID("mechanical_hazard", 13),
|
||||
Category: "mechanical_hazard",
|
||||
SubCategory: "herabfallende_teile",
|
||||
Name: "Herabfallende Teile aus Werkstueckhalterung",
|
||||
Description: "Unzureichend gesicherte Werkstuecke oder Werkzeuge koennen sich aus der Halterung loesen und herabfallen.",
|
||||
DefaultSeverity: 4,
|
||||
DefaultProbability: 2,
|
||||
DefaultExposure: 3,
|
||||
DefaultAvoidance: 3,
|
||||
ApplicableComponentTypes: []string{"mechanical"},
|
||||
RegulationReferences: []string{"Maschinenverordnung 2023/1230 Anhang I"},
|
||||
SuggestedMitigations: mustMarshalJSON([]string{"Spannkraftueberwachung der Halterung", "Schutzdach ueber dem Bedienerbereich"}),
|
||||
TypicalCauses: []string{"Unzureichende Spannkraft", "Vibration lockert die Halterung", "Falsches Werkstueck-Spannmittel"},
|
||||
TypicalHarm: "Kopfverletzungen, Prellungen, Quetschungen durch herabfallende Teile",
|
||||
RelevantLifecyclePhases: []string{"normal_operation", "setup"},
|
||||
RecommendedMeasuresDesign: []string{"Spannkraftueberwachung mit Abschaltung", "Auffangvorrichtung unter Werkstueck"},
|
||||
RecommendedMeasuresTechnical: []string{"Sensor zur Spannkraftueberwachung", "Schutzhaube"},
|
||||
RecommendedMeasuresInformation: []string{"Pruefanweisung vor Bearbeitungsstart", "Schutzhelmpflicht im Gefahrenbereich"},
|
||||
SuggestedEvidence: []string{"Pruefprotokoll Spannmittel", "Risikobeurteilung"},
|
||||
RelatedKeywords: []string{"Werkstueck", "Spannmittel", "Herabfallen"},
|
||||
IsBuiltin: true,
|
||||
TenantID: nil,
|
||||
CreatedAt: now,
|
||||
},
|
||||
{
|
||||
ID: hazardUUID("mechanical_hazard", 14),
|
||||
Category: "mechanical_hazard",
|
||||
SubCategory: "wegschleudern",
|
||||
Name: "Wegschleudern von Bruchstuecken bei Werkzeugversagen",
|
||||
Description: "Bei Werkzeugbruch koennen Bruchstuecke mit hoher Geschwindigkeit weggeschleudert werden und Personen im Umfeld verletzen.",
|
||||
DefaultSeverity: 5,
|
||||
DefaultProbability: 2,
|
||||
DefaultExposure: 3,
|
||||
DefaultAvoidance: 2,
|
||||
ApplicableComponentTypes: []string{"mechanical"},
|
||||
RegulationReferences: []string{"Maschinenverordnung 2023/1230 Anhang I"},
|
||||
SuggestedMitigations: mustMarshalJSON([]string{"Splitterschutzscheibe aus Polycarbonat", "Regelmaessige Werkzeuginspektion"}),
|
||||
TypicalCauses: []string{"Werkzeugverschleiss", "Ueberschreitung der zulaessigen Drehzahl", "Materialfehler im Werkzeug"},
|
||||
TypicalHarm: "Durchdringende Verletzungen durch Bruchstuecke, Augenverletzungen",
|
||||
RelevantLifecyclePhases: []string{"normal_operation"},
|
||||
RecommendedMeasuresDesign: []string{"Splitterschutz in der Einhausung", "Drehzahlbegrenzung des Werkzeugs"},
|
||||
RecommendedMeasuresTechnical: []string{"Unwuchtueberwachung", "Brucherkennungssensor"},
|
||||
RecommendedMeasuresInformation: []string{"Maximaldrehzahl am Werkzeug kennzeichnen", "Schutzbrillenpflicht"},
|
||||
SuggestedEvidence: []string{"Bersttest der Einhausung", "Werkzeuginspektionsprotokoll"},
|
||||
RelatedKeywords: []string{"Werkzeugbruch", "Splitter", "Schleudern"},
|
||||
IsBuiltin: true,
|
||||
TenantID: nil,
|
||||
CreatedAt: now,
|
||||
},
|
||||
{
|
||||
ID: hazardUUID("mechanical_hazard", 15),
|
||||
Category: "mechanical_hazard",
|
||||
SubCategory: "instabilitaet",
|
||||
Name: "Instabilitaet der Maschine durch fehlendes Fundament",
|
||||
Description: "Eine unzureichend verankerte oder falsch aufgestellte Maschine kann kippen oder sich verschieben, insbesondere bei dynamischen Kraeften.",
|
||||
DefaultSeverity: 4,
|
||||
DefaultProbability: 2,
|
||||
DefaultExposure: 2,
|
||||
DefaultAvoidance: 3,
|
||||
ApplicableComponentTypes: []string{"mechanical"},
|
||||
RegulationReferences: []string{"Maschinenverordnung 2023/1230 Anhang I"},
|
||||
SuggestedMitigations: mustMarshalJSON([]string{"Fundamentberechnung und Verankerung", "Standsicherheitsnachweis"}),
|
||||
TypicalCauses: []string{"Fehlende Bodenverankerung", "Ungeeigneter Untergrund", "Erhoehte dynamische Lasten"},
|
||||
TypicalHarm: "Quetschverletzungen durch kippende Maschine, Sachschaeden",
|
||||
RelevantLifecyclePhases: []string{"installation", "normal_operation", "transport"},
|
||||
RecommendedMeasuresDesign: []string{"Niedriger Schwerpunkt der Maschine", "Befestigungspunkte im Maschinenrahmen"},
|
||||
RecommendedMeasuresTechnical: []string{"Bodenverankerung mit Schwerlastduebeln", "Nivellierelemente mit Kippsicherung"},
|
||||
RecommendedMeasuresInformation: []string{"Aufstellanleitung mit Fundamentplan", "Hinweis auf maximale Bodenbelastung"},
|
||||
SuggestedEvidence: []string{"Standsicherheitsnachweis", "Fundamentplan"},
|
||||
RelatedKeywords: []string{"Fundament", "Standsicherheit", "Kippen"},
|
||||
IsBuiltin: true,
|
||||
TenantID: nil,
|
||||
CreatedAt: now,
|
||||
},
|
||||
{
|
||||
ID: hazardUUID("mechanical_hazard", 16),
|
||||
Category: "mechanical_hazard",
|
||||
SubCategory: "wiederanlauf",
|
||||
Name: "Unkontrollierter Wiederanlauf nach Energieunterbruch",
|
||||
Description: "Nach einem Stromausfall oder Druckabfall kann die Maschine unkontrolliert wieder anlaufen und Personen im Gefahrenbereich verletzen.",
|
||||
DefaultSeverity: 5,
|
||||
DefaultProbability: 3,
|
||||
DefaultExposure: 3,
|
||||
DefaultAvoidance: 2,
|
||||
ApplicableComponentTypes: []string{"mechanical", "controller", "electrical"},
|
||||
RegulationReferences: []string{"Maschinenverordnung 2023/1230 Anhang I"},
|
||||
SuggestedMitigations: mustMarshalJSON([]string{"Wiederanlaufsperre nach Energierueckkehr", "Quittierungspflichtiger Neustart"}),
|
||||
TypicalCauses: []string{"Fehlende Wiederanlaufsperre", "Stromausfall mit anschliessendem automatischem Neustart", "Druckaufbau nach Leckagereparatur"},
|
||||
TypicalHarm: "Verletzungen durch unerwartete Maschinenbewegung bei Wiederanlauf",
|
||||
RelevantLifecyclePhases: []string{"normal_operation", "maintenance", "fault_finding"},
|
||||
RecommendedMeasuresDesign: []string{"Wiederanlaufsperre in der Steuerung", "Energiespeicher sicher entladen"},
|
||||
RecommendedMeasuresTechnical: []string{"Schaltschuetz mit Selbsthaltung", "Druckschalter mit Ruecksetzbedingung"},
|
||||
RecommendedMeasuresInformation: []string{"Hinweis auf Wiederanlaufverhalten", "Verfahrensanweisung nach Energieausfall"},
|
||||
SuggestedEvidence: []string{"Funktionstest Wiederanlaufsperre", "Risikobeurteilung"},
|
||||
RelatedKeywords: []string{"Wiederanlauf", "Stromausfall", "Anlaufsperre"},
|
||||
IsBuiltin: true,
|
||||
TenantID: nil,
|
||||
CreatedAt: now,
|
||||
},
|
||||
{
|
||||
ID: hazardUUID("mechanical_hazard", 17),
|
||||
Category: "mechanical_hazard",
|
||||
SubCategory: "reibungsgefahr",
|
||||
Name: "Reibungsgefahr an rauen Oberflaechen",
|
||||
Description: "Raue, scharfkantige oder gratbehaftete Maschinenoberlaechen koennen bei Kontakt zu Hautabschuerfungen und Schnittverletzungen fuehren.",
|
||||
DefaultSeverity: 3,
|
||||
DefaultProbability: 3,
|
||||
DefaultExposure: 4,
|
||||
DefaultAvoidance: 4,
|
||||
ApplicableComponentTypes: []string{"mechanical"},
|
||||
RegulationReferences: []string{"Maschinenverordnung 2023/1230 Anhang I"},
|
||||
SuggestedMitigations: mustMarshalJSON([]string{"Entgraten aller zugaenglichen Kanten", "Schutzhandschuhe fuer Bedienpersonal"}),
|
||||
TypicalCauses: []string{"Nicht entgratete Schnittkanten", "Korrosionsraue Oberflaechen", "Verschleissbedingter Materialabtrag"},
|
||||
TypicalHarm: "Hautabschuerfungen, Schnittverletzungen an Haenden und Armen",
|
||||
RelevantLifecyclePhases: []string{"normal_operation", "maintenance", "setup"},
|
||||
RecommendedMeasuresDesign: []string{"Kanten brechen oder abrunden", "Glatte Oberflaechen an Kontaktstellen"},
|
||||
RecommendedMeasuresTechnical: []string{"Kantenschutzprofile anbringen"},
|
||||
RecommendedMeasuresInformation: []string{"Hinweis auf scharfe Kanten", "Handschuhpflicht in der Betriebsanweisung"},
|
||||
SuggestedEvidence: []string{"Oberflaechenpruefung", "Risikobeurteilung"},
|
||||
RelatedKeywords: []string{"Grat", "Scharfkantig", "Oberflaeche"},
|
||||
IsBuiltin: true,
|
||||
TenantID: nil,
|
||||
CreatedAt: now,
|
||||
},
|
||||
{
|
||||
ID: hazardUUID("mechanical_hazard", 18),
|
||||
Category: "mechanical_hazard",
|
||||
SubCategory: "hochdruckstrahl",
|
||||
Name: "Fluessigkeitshochdruckstrahl",
|
||||
Description: "Hochdruckstrahlen aus Hydraulik-, Kuehl- oder Reinigungssystemen koennen Haut durchdringen und schwere Gewebeschaeden verursachen.",
|
||||
DefaultSeverity: 5,
|
||||
DefaultProbability: 2,
|
||||
DefaultExposure: 2,
|
||||
DefaultAvoidance: 2,
|
||||
ApplicableComponentTypes: []string{"mechanical", "actuator"},
|
||||
RegulationReferences: []string{"Maschinenverordnung 2023/1230 Anhang I"},
|
||||
SuggestedMitigations: mustMarshalJSON([]string{"Abschirmung von Hochdruckleitungen", "Regelmaessige Leitungsinspektion"}),
|
||||
TypicalCauses: []string{"Leitungsbruch unter Hochdruck", "Undichte Verschraubungen", "Alterung von Schlauchleitungen"},
|
||||
TypicalHarm: "Hochdruckinjektionsverletzungen, Gewebsnekrose",
|
||||
RelevantLifecyclePhases: []string{"normal_operation", "maintenance"},
|
||||
RecommendedMeasuresDesign: []string{"Schlauchbruchsicherungen einbauen", "Leitungen ausserhalb des Aufenthaltsbereichs verlegen"},
|
||||
RecommendedMeasuresTechnical: []string{"Druckabschaltung bei Leitungsbruch", "Schutzblechverkleidung"},
|
||||
RecommendedMeasuresInformation: []string{"Warnhinweis an Hochdruckleitungen", "Prueffristen fuer Schlauchleitungen"},
|
||||
SuggestedEvidence: []string{"Druckpruefprotokoll", "Inspektionsbericht Schlauchleitungen"},
|
||||
RelatedKeywords: []string{"Hochdruck", "Hydraulikleitung", "Injection"},
|
||||
IsBuiltin: true,
|
||||
TenantID: nil,
|
||||
CreatedAt: now,
|
||||
},
|
||||
{
|
||||
ID: hazardUUID("mechanical_hazard", 19),
|
||||
Category: "mechanical_hazard",
|
||||
SubCategory: "federelemente",
|
||||
Name: "Gefahr durch federgespannte Elemente",
|
||||
Description: "Unter Spannung stehende Federn oder elastische Elemente koennen bei unkontrolliertem Loesen Teile wegschleudern oder Personen verletzen.",
|
||||
DefaultSeverity: 4,
|
||||
DefaultProbability: 2,
|
||||
DefaultExposure: 2,
|
||||
DefaultAvoidance: 3,
|
||||
ApplicableComponentTypes: []string{"mechanical"},
|
||||
RegulationReferences: []string{"Maschinenverordnung 2023/1230 Anhang I"},
|
||||
SuggestedMitigations: mustMarshalJSON([]string{"Gesicherte Federentspannung vor Demontage", "Warnung bei vorgespannten Elementen"}),
|
||||
TypicalCauses: []string{"Demontage ohne vorherige Entspannung", "Materialermuedung der Feder", "Fehlende Kennzeichnung vorgespannter Elemente"},
|
||||
TypicalHarm: "Verletzungen durch wegschleudernde Federelemente, Prellungen",
|
||||
RelevantLifecyclePhases: []string{"maintenance", "decommissioning"},
|
||||
RecommendedMeasuresDesign: []string{"Sichere Entspannungsmoeglichkeit vorsehen", "Federn mit Bruchsicherung"},
|
||||
RecommendedMeasuresTechnical: []string{"Spezialwerkzeug zur Federentspannung"},
|
||||
RecommendedMeasuresInformation: []string{"Kennzeichnung vorgespannter Elemente", "Wartungsanweisung mit Entspannungsprozedur"},
|
||||
SuggestedEvidence: []string{"Wartungsanweisung", "Risikobeurteilung"},
|
||||
RelatedKeywords: []string{"Feder", "Vorspannung", "Energiespeicher"},
|
||||
IsBuiltin: true,
|
||||
TenantID: nil,
|
||||
CreatedAt: now,
|
||||
},
|
||||
{
|
||||
ID: hazardUUID("mechanical_hazard", 20),
|
||||
Category: "mechanical_hazard",
|
||||
SubCategory: "schutztor",
|
||||
Name: "Quetschgefahr im Schliessbereich von Schutztoren",
|
||||
Description: "Automatisch schliessende Schutztore und -tueren koennen Personen im Schliessbereich einklemmen oder quetschen.",
|
||||
DefaultSeverity: 4,
|
||||
DefaultProbability: 3,
|
||||
DefaultExposure: 4,
|
||||
DefaultAvoidance: 3,
|
||||
ApplicableComponentTypes: []string{"mechanical", "actuator", "sensor"},
|
||||
RegulationReferences: []string{"Maschinenverordnung 2023/1230 Anhang I"},
|
||||
SuggestedMitigations: mustMarshalJSON([]string{"Schliesskantensicherung mit Kontaktleiste", "Lichtschranke im Schliessbereich"}),
|
||||
TypicalCauses: []string{"Fehlende Schliesskantensicherung", "Defekter Sensor", "Person im Schliessbereich nicht erkannt"},
|
||||
TypicalHarm: "Quetschverletzungen an Koerper oder Gliedmassen",
|
||||
RelevantLifecyclePhases: []string{"normal_operation", "maintenance"},
|
||||
RecommendedMeasuresDesign: []string{"Schliesskraftbegrenzung", "Reversierautomatik bei Hindernis"},
|
||||
RecommendedMeasuresTechnical: []string{"Kontaktleiste an der Schliesskante", "Lichtschranke im Durchgangsbereich"},
|
||||
RecommendedMeasuresInformation: []string{"Warnhinweis am Schutztor", "Automatik-Betrieb kennzeichnen"},
|
||||
SuggestedEvidence: []string{"Schliesskraftmessung", "Funktionstest Reversierautomatik"},
|
||||
RelatedKeywords: []string{"Schutztor", "Schliesskante", "Einklemmen"},
|
||||
IsBuiltin: true,
|
||||
TenantID: nil,
|
||||
CreatedAt: now,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
}
|
||||
}
|
||||
597
ai-compliance-sdk/internal/iace/hazard_library_machine_safety.go
Normal file
597
ai-compliance-sdk/internal/iace/hazard_library_machine_safety.go
Normal 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,
|
||||
},
|
||||
}
|
||||
}
|
||||
578
ai-compliance-sdk/internal/iace/hazard_library_software_hmi.go
Normal file
578
ai-compliance-sdk/internal/iace/hazard_library_software_hmi.go
Normal 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
383
ai-compliance-sdk/internal/iace/store_audit.go
Normal file
383
ai-compliance-sdk/internal/iace/store_audit.go
Normal 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
|
||||
}
|
||||
555
ai-compliance-sdk/internal/iace/store_hazards.go
Normal file
555
ai-compliance-sdk/internal/iace/store_hazards.go
Normal 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, ®ulationReferences,
|
||||
&suggestedMitigations,
|
||||
&typicalCauses, &e.TypicalHarm, &relevantPhases,
|
||||
&measuresDesign, &measuresTechnical, &measuresInfo,
|
||||
&evidence, &keywords,
|
||||
&e.IsBuiltin, &e.TenantID,
|
||||
&e.CreatedAt,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("list hazard library scan: %w", err)
|
||||
}
|
||||
|
||||
json.Unmarshal(applicableComponentTypes, &e.ApplicableComponentTypes)
|
||||
json.Unmarshal(regulationReferences, &e.RegulationReferences)
|
||||
json.Unmarshal(suggestedMitigations, &e.SuggestedMitigations)
|
||||
json.Unmarshal(typicalCauses, &e.TypicalCauses)
|
||||
json.Unmarshal(relevantPhases, &e.RelevantLifecyclePhases)
|
||||
json.Unmarshal(measuresDesign, &e.RecommendedMeasuresDesign)
|
||||
json.Unmarshal(measuresTechnical, &e.RecommendedMeasuresTechnical)
|
||||
json.Unmarshal(measuresInfo, &e.RecommendedMeasuresInformation)
|
||||
json.Unmarshal(evidence, &e.SuggestedEvidence)
|
||||
json.Unmarshal(keywords, &e.RelatedKeywords)
|
||||
|
||||
if e.ApplicableComponentTypes == nil {
|
||||
e.ApplicableComponentTypes = []string{}
|
||||
}
|
||||
if e.RegulationReferences == nil {
|
||||
e.RegulationReferences = []string{}
|
||||
}
|
||||
|
||||
entries = append(entries, e)
|
||||
}
|
||||
|
||||
return entries, nil
|
||||
}
|
||||
|
||||
// GetHazardLibraryEntry retrieves a single hazard library entry by ID
|
||||
func (s *Store) GetHazardLibraryEntry(ctx context.Context, id uuid.UUID) (*HazardLibraryEntry, error) {
|
||||
var e HazardLibraryEntry
|
||||
var applicableComponentTypes, regulationReferences, suggestedMitigations []byte
|
||||
var typicalCauses, relevantLifecyclePhases []byte
|
||||
var recommendedMeasuresDesign, recommendedMeasuresTechnical, recommendedMeasuresInformation []byte
|
||||
var suggestedEvidence, relatedKeywords []byte
|
||||
|
||||
err := s.pool.QueryRow(ctx, `
|
||||
SELECT
|
||||
id, category, name, description,
|
||||
default_severity, default_probability,
|
||||
applicable_component_types, regulation_references,
|
||||
suggested_mitigations, is_builtin, tenant_id,
|
||||
created_at,
|
||||
COALESCE(sub_category, ''),
|
||||
COALESCE(default_exposure, 3),
|
||||
COALESCE(default_avoidance, 3),
|
||||
COALESCE(typical_causes, '[]'),
|
||||
COALESCE(typical_harm, ''),
|
||||
COALESCE(relevant_lifecycle_phases, '[]'),
|
||||
COALESCE(recommended_measures_design, '[]'),
|
||||
COALESCE(recommended_measures_technical, '[]'),
|
||||
COALESCE(recommended_measures_information, '[]'),
|
||||
COALESCE(suggested_evidence, '[]'),
|
||||
COALESCE(related_keywords, '[]')
|
||||
FROM iace_hazard_library WHERE id = $1
|
||||
`, id).Scan(
|
||||
&e.ID, &e.Category, &e.Name, &e.Description,
|
||||
&e.DefaultSeverity, &e.DefaultProbability,
|
||||
&applicableComponentTypes, ®ulationReferences,
|
||||
&suggestedMitigations, &e.IsBuiltin, &e.TenantID,
|
||||
&e.CreatedAt,
|
||||
&e.SubCategory,
|
||||
&e.DefaultExposure, &e.DefaultAvoidance,
|
||||
&typicalCauses, &e.TypicalHarm,
|
||||
&relevantLifecyclePhases,
|
||||
&recommendedMeasuresDesign, &recommendedMeasuresTechnical, &recommendedMeasuresInformation,
|
||||
&suggestedEvidence, &relatedKeywords,
|
||||
)
|
||||
if err == pgx.ErrNoRows {
|
||||
return nil, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("get hazard library entry: %w", err)
|
||||
}
|
||||
|
||||
json.Unmarshal(applicableComponentTypes, &e.ApplicableComponentTypes)
|
||||
json.Unmarshal(regulationReferences, &e.RegulationReferences)
|
||||
json.Unmarshal(suggestedMitigations, &e.SuggestedMitigations)
|
||||
json.Unmarshal(typicalCauses, &e.TypicalCauses)
|
||||
json.Unmarshal(relevantLifecyclePhases, &e.RelevantLifecyclePhases)
|
||||
json.Unmarshal(recommendedMeasuresDesign, &e.RecommendedMeasuresDesign)
|
||||
json.Unmarshal(recommendedMeasuresTechnical, &e.RecommendedMeasuresTechnical)
|
||||
json.Unmarshal(recommendedMeasuresInformation, &e.RecommendedMeasuresInformation)
|
||||
json.Unmarshal(suggestedEvidence, &e.SuggestedEvidence)
|
||||
json.Unmarshal(relatedKeywords, &e.RelatedKeywords)
|
||||
|
||||
if e.ApplicableComponentTypes == nil {
|
||||
e.ApplicableComponentTypes = []string{}
|
||||
}
|
||||
if e.RegulationReferences == nil {
|
||||
e.RegulationReferences = []string{}
|
||||
}
|
||||
|
||||
return &e, nil
|
||||
}
|
||||
506
ai-compliance-sdk/internal/iace/store_mitigations.go
Normal file
506
ai-compliance-sdk/internal/iace/store_mitigations.go
Normal 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
|
||||
}
|
||||
529
ai-compliance-sdk/internal/iace/store_projects.go
Normal file
529
ai-compliance-sdk/internal/iace/store_projects.go
Normal 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, ®, &c.ClassificationResult,
|
||||
&rl, &c.Confidence, &c.Reasoning,
|
||||
&ragSources, &requirements,
|
||||
&c.CreatedAt, &c.UpdatedAt,
|
||||
)
|
||||
if err == pgx.ErrNoRows {
|
||||
return nil, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("get classification: %w", err)
|
||||
}
|
||||
|
||||
c.Regulation = RegulationType(reg)
|
||||
c.RiskLevel = RiskLevel(rl)
|
||||
json.Unmarshal(ragSources, &c.RAGSources)
|
||||
json.Unmarshal(requirements, &c.Requirements)
|
||||
|
||||
return &c, nil
|
||||
}
|
||||
|
||||
// GetClassifications retrieves all classifications for a project
|
||||
func (s *Store) GetClassifications(ctx context.Context, projectID uuid.UUID) ([]RegulatoryClassification, error) {
|
||||
rows, err := s.pool.Query(ctx, `
|
||||
SELECT
|
||||
id, project_id, regulation, classification_result,
|
||||
risk_level, confidence, reasoning,
|
||||
rag_sources, requirements,
|
||||
created_at, updated_at
|
||||
FROM iace_classifications
|
||||
WHERE project_id = $1
|
||||
ORDER BY regulation ASC
|
||||
`, projectID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("get classifications: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var classifications []RegulatoryClassification
|
||||
for rows.Next() {
|
||||
var c RegulatoryClassification
|
||||
var reg, rl string
|
||||
var ragSources, requirements []byte
|
||||
|
||||
err := rows.Scan(
|
||||
&c.ID, &c.ProjectID, ®, &c.ClassificationResult,
|
||||
&rl, &c.Confidence, &c.Reasoning,
|
||||
&ragSources, &requirements,
|
||||
&c.CreatedAt, &c.UpdatedAt,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("get classifications scan: %w", err)
|
||||
}
|
||||
|
||||
c.Regulation = RegulationType(reg)
|
||||
c.RiskLevel = RiskLevel(rl)
|
||||
json.Unmarshal(ragSources, &c.RAGSources)
|
||||
json.Unmarshal(requirements, &c.Requirements)
|
||||
|
||||
classifications = append(classifications, c)
|
||||
}
|
||||
|
||||
return classifications, nil
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
340
ai-compliance-sdk/internal/training/store_assignments.go
Normal file
340
ai-compliance-sdk/internal/training/store_assignments.go
Normal 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
|
||||
}
|
||||
128
ai-compliance-sdk/internal/training/store_audit.go
Normal file
128
ai-compliance-sdk/internal/training/store_audit.go
Normal 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
|
||||
}
|
||||
198
ai-compliance-sdk/internal/training/store_checkpoints.go
Normal file
198
ai-compliance-sdk/internal/training/store_checkpoints.go
Normal 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
|
||||
}
|
||||
130
ai-compliance-sdk/internal/training/store_content.go
Normal file
130
ai-compliance-sdk/internal/training/store_content.go
Normal 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
|
||||
}
|
||||
112
ai-compliance-sdk/internal/training/store_matrix.go
Normal file
112
ai-compliance-sdk/internal/training/store_matrix.go
Normal 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
|
||||
}
|
||||
192
ai-compliance-sdk/internal/training/store_media.go
Normal file
192
ai-compliance-sdk/internal/training/store_media.go
Normal 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
|
||||
}
|
||||
235
ai-compliance-sdk/internal/training/store_modules.go
Normal file
235
ai-compliance-sdk/internal/training/store_modules.go
Normal 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,
|
||||
®ulationArea, &module.NIS2Relevant, &isoControls, &frequencyType,
|
||||
&module.ValidityDays, &module.RiskWeight, &module.ContentType, &module.DurationMinutes,
|
||||
&module.PassThreshold, &module.IsActive, &module.SortOrder, &module.CreatedAt, &module.UpdatedAt,
|
||||
)
|
||||
|
||||
if err == pgx.ErrNoRows {
|
||||
return nil, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
module.RegulationArea = RegulationArea(regulationArea)
|
||||
module.FrequencyType = FrequencyType(frequencyType)
|
||||
json.Unmarshal(isoControls, &module.ISOControls)
|
||||
if module.ISOControls == nil {
|
||||
module.ISOControls = []string{}
|
||||
}
|
||||
|
||||
return &module, nil
|
||||
}
|
||||
|
||||
// ListModules lists training modules for a tenant with optional filters
|
||||
func (s *Store) ListModules(ctx context.Context, tenantID uuid.UUID, filters *ModuleFilters) ([]TrainingModule, int, error) {
|
||||
countQuery := "SELECT COUNT(*) FROM training_modules WHERE tenant_id = $1"
|
||||
countArgs := []interface{}{tenantID}
|
||||
countArgIdx := 2
|
||||
|
||||
query := `
|
||||
SELECT
|
||||
id, tenant_id, academy_course_id, module_code, title, description,
|
||||
regulation_area, nis2_relevant, iso_controls, frequency_type,
|
||||
validity_days, risk_weight, content_type, duration_minutes,
|
||||
pass_threshold, is_active, sort_order, created_at, updated_at
|
||||
FROM training_modules WHERE tenant_id = $1`
|
||||
|
||||
args := []interface{}{tenantID}
|
||||
argIdx := 2
|
||||
|
||||
if filters != nil {
|
||||
if filters.RegulationArea != "" {
|
||||
query += fmt.Sprintf(" AND regulation_area = $%d", argIdx)
|
||||
args = append(args, string(filters.RegulationArea))
|
||||
argIdx++
|
||||
countQuery += fmt.Sprintf(" AND regulation_area = $%d", countArgIdx)
|
||||
countArgs = append(countArgs, string(filters.RegulationArea))
|
||||
countArgIdx++
|
||||
}
|
||||
if filters.FrequencyType != "" {
|
||||
query += fmt.Sprintf(" AND frequency_type = $%d", argIdx)
|
||||
args = append(args, string(filters.FrequencyType))
|
||||
argIdx++
|
||||
countQuery += fmt.Sprintf(" AND frequency_type = $%d", countArgIdx)
|
||||
countArgs = append(countArgs, string(filters.FrequencyType))
|
||||
countArgIdx++
|
||||
}
|
||||
if filters.IsActive != nil {
|
||||
query += fmt.Sprintf(" AND is_active = $%d", argIdx)
|
||||
args = append(args, *filters.IsActive)
|
||||
argIdx++
|
||||
countQuery += fmt.Sprintf(" AND is_active = $%d", countArgIdx)
|
||||
countArgs = append(countArgs, *filters.IsActive)
|
||||
countArgIdx++
|
||||
}
|
||||
if filters.NIS2Relevant != nil {
|
||||
query += fmt.Sprintf(" AND nis2_relevant = $%d", argIdx)
|
||||
args = append(args, *filters.NIS2Relevant)
|
||||
argIdx++
|
||||
countQuery += fmt.Sprintf(" AND nis2_relevant = $%d", countArgIdx)
|
||||
countArgs = append(countArgs, *filters.NIS2Relevant)
|
||||
countArgIdx++
|
||||
}
|
||||
if filters.Search != "" {
|
||||
query += fmt.Sprintf(" AND (title ILIKE $%d OR description ILIKE $%d OR module_code ILIKE $%d)", argIdx, argIdx, argIdx)
|
||||
args = append(args, "%"+filters.Search+"%")
|
||||
argIdx++
|
||||
countQuery += fmt.Sprintf(" AND (title ILIKE $%d OR description ILIKE $%d OR module_code ILIKE $%d)", countArgIdx, countArgIdx, countArgIdx)
|
||||
countArgs = append(countArgs, "%"+filters.Search+"%")
|
||||
countArgIdx++
|
||||
}
|
||||
}
|
||||
|
||||
var total int
|
||||
err := s.pool.QueryRow(ctx, countQuery, countArgs...).Scan(&total)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
query += " ORDER BY sort_order ASC, created_at DESC"
|
||||
|
||||
if filters != nil && filters.Limit > 0 {
|
||||
query += fmt.Sprintf(" LIMIT $%d", argIdx)
|
||||
args = append(args, filters.Limit)
|
||||
argIdx++
|
||||
if filters.Offset > 0 {
|
||||
query += fmt.Sprintf(" OFFSET $%d", argIdx)
|
||||
args = append(args, filters.Offset)
|
||||
argIdx++
|
||||
}
|
||||
}
|
||||
|
||||
rows, err := s.pool.Query(ctx, query, args...)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var modules []TrainingModule
|
||||
for rows.Next() {
|
||||
var module TrainingModule
|
||||
var regulationArea, frequencyType string
|
||||
var isoControls []byte
|
||||
|
||||
err := rows.Scan(
|
||||
&module.ID, &module.TenantID, &module.AcademyCourseID, &module.ModuleCode, &module.Title, &module.Description,
|
||||
®ulationArea, &module.NIS2Relevant, &isoControls, &frequencyType,
|
||||
&module.ValidityDays, &module.RiskWeight, &module.ContentType, &module.DurationMinutes,
|
||||
&module.PassThreshold, &module.IsActive, &module.SortOrder, &module.CreatedAt, &module.UpdatedAt,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
module.RegulationArea = RegulationArea(regulationArea)
|
||||
module.FrequencyType = FrequencyType(frequencyType)
|
||||
json.Unmarshal(isoControls, &module.ISOControls)
|
||||
if module.ISOControls == nil {
|
||||
module.ISOControls = []string{}
|
||||
}
|
||||
|
||||
modules = append(modules, module)
|
||||
}
|
||||
|
||||
if modules == nil {
|
||||
modules = []TrainingModule{}
|
||||
}
|
||||
|
||||
return modules, total, nil
|
||||
}
|
||||
|
||||
// UpdateModule updates a training module
|
||||
func (s *Store) UpdateModule(ctx context.Context, module *TrainingModule) error {
|
||||
module.UpdatedAt = time.Now().UTC()
|
||||
isoControls, _ := json.Marshal(module.ISOControls)
|
||||
|
||||
_, err := s.pool.Exec(ctx, `
|
||||
UPDATE training_modules SET
|
||||
title = $2, description = $3, nis2_relevant = $4,
|
||||
iso_controls = $5, validity_days = $6, risk_weight = $7,
|
||||
duration_minutes = $8, pass_threshold = $9, is_active = $10,
|
||||
sort_order = $11, updated_at = $12
|
||||
WHERE id = $1
|
||||
`,
|
||||
module.ID, module.Title, module.Description, module.NIS2Relevant,
|
||||
isoControls, module.ValidityDays, module.RiskWeight,
|
||||
module.DurationMinutes, module.PassThreshold, module.IsActive,
|
||||
module.SortOrder, module.UpdatedAt,
|
||||
)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// DeleteModule deletes a training module by ID
|
||||
func (s *Store) DeleteModule(ctx context.Context, id uuid.UUID) error {
|
||||
_, err := s.pool.Exec(ctx, `DELETE FROM training_modules WHERE id = $1`, id)
|
||||
return err
|
||||
}
|
||||
|
||||
// SetAcademyCourseID links a training module to an academy course
|
||||
func (s *Store) SetAcademyCourseID(ctx context.Context, moduleID, courseID uuid.UUID) error {
|
||||
_, err := s.pool.Exec(ctx, `
|
||||
UPDATE training_modules SET academy_course_id = $2, updated_at = $3 WHERE id = $1
|
||||
`, moduleID, courseID, time.Now().UTC())
|
||||
return err
|
||||
}
|
||||
140
ai-compliance-sdk/internal/training/store_quiz.go
Normal file
140
ai-compliance-sdk/internal/training/store_quiz.go
Normal 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
|
||||
}
|
||||
120
ai-compliance-sdk/internal/training/store_stats.go
Normal file
120
ai-compliance-sdk/internal/training/store_stats.go
Normal 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
|
||||
}
|
||||
@@ -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."
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
128
ai-compliance-sdk/internal/ucca/rules_controls.go
Normal file
128
ai-compliance-sdk/internal/ucca/rules_controls.go
Normal 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
|
||||
}
|
||||
499
ai-compliance-sdk/internal/ucca/rules_data.go
Normal file
499
ai-compliance-sdk/internal/ucca/rules_data.go
Normal 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."
|
||||
},
|
||||
},
|
||||
}
|
||||
323
ai-compliance-sdk/internal/ucca/rules_data_fj.go
Normal file
323
ai-compliance-sdk/internal/ucca/rules_data_fj.go
Normal file
@@ -0,0 +1,323 @@
|
||||
package ucca
|
||||
|
||||
func init() {
|
||||
AllRules = append(AllRules, rulesFJ()...)
|
||||
}
|
||||
|
||||
// rulesFJ returns rules for categories F–J (R-050 to R-100)
|
||||
func rulesFJ() []Rule {
|
||||
return []Rule{
|
||||
// =========================================================================
|
||||
// F. Hosting (R-050 bis R-052)
|
||||
// =========================================================================
|
||||
{
|
||||
Code: "R-050",
|
||||
Category: "F. Hosting",
|
||||
Title: "Third Country Transfer with PII",
|
||||
TitleDE: "Drittlandtransfer mit PII",
|
||||
Description: "Transferring PII to third countries requires safeguards",
|
||||
DescriptionDE: "Übermittlung von PII in Drittländer erfordert Schutzmaßnahmen",
|
||||
Severity: SeverityWARN,
|
||||
ScoreDelta: 20,
|
||||
GDPRRef: "Art. 44-49 DSGVO",
|
||||
Controls: []string{"C-SCC", "C-ENCRYPTION"},
|
||||
Patterns: []string{},
|
||||
Condition: func(intake *UseCaseIntake) bool {
|
||||
return intake.Hosting.Region == "third_country" && intake.DataTypes.PersonalData
|
||||
},
|
||||
Rationale: func(intake *UseCaseIntake) string {
|
||||
return "Übermittlung personenbezogener Daten in Drittländer erfordert Standardvertragsklauseln oder andere geeignete Garantien."
|
||||
},
|
||||
},
|
||||
{
|
||||
Code: "R-051",
|
||||
Category: "F. Hosting",
|
||||
Title: "EU Hosting - Compliant",
|
||||
TitleDE: "EU-Hosting - Konform",
|
||||
Description: "Hosting within EU is compliant with GDPR",
|
||||
DescriptionDE: "Hosting innerhalb der EU ist DSGVO-konform",
|
||||
Severity: SeverityINFO,
|
||||
ScoreDelta: 0,
|
||||
GDPRRef: "",
|
||||
Controls: []string{},
|
||||
Patterns: []string{},
|
||||
Condition: func(intake *UseCaseIntake) bool {
|
||||
return intake.Hosting.Region == "eu"
|
||||
},
|
||||
Rationale: func(intake *UseCaseIntake) string {
|
||||
return "Hosting innerhalb der EU/EWR erfüllt grundsätzlich die DSGVO-Anforderungen an den Datenstandort."
|
||||
},
|
||||
},
|
||||
{
|
||||
Code: "R-052",
|
||||
Category: "F. Hosting",
|
||||
Title: "On-Premise Hosting",
|
||||
TitleDE: "On-Premise-Hosting",
|
||||
Description: "On-premise hosting gives most control",
|
||||
DescriptionDE: "On-Premise-Hosting gibt die meiste Kontrolle",
|
||||
Severity: SeverityINFO,
|
||||
ScoreDelta: 0,
|
||||
GDPRRef: "",
|
||||
Controls: []string{"C-ENCRYPTION"},
|
||||
Patterns: []string{},
|
||||
Condition: func(intake *UseCaseIntake) bool {
|
||||
return intake.Hosting.Region == "on_prem"
|
||||
},
|
||||
Rationale: func(intake *UseCaseIntake) string {
|
||||
return "On-Premise-Hosting bietet maximale Kontrolle über Daten, erfordert aber eigene Sicherheitsmaßnahmen."
|
||||
},
|
||||
},
|
||||
// =========================================================================
|
||||
// G. Transparenz (R-060 bis R-062)
|
||||
// =========================================================================
|
||||
{
|
||||
Code: "R-060",
|
||||
Category: "G. Transparenz",
|
||||
Title: "No Human Review for Decisions",
|
||||
TitleDE: "Keine menschliche Überprüfung bei Entscheidungen",
|
||||
Description: "Decisions affecting individuals need human review option",
|
||||
DescriptionDE: "Entscheidungen, die Personen betreffen, benötigen menschliche Überprüfungsoption",
|
||||
Severity: SeverityWARN,
|
||||
ScoreDelta: 15,
|
||||
GDPRRef: "Art. 22(3) DSGVO",
|
||||
Controls: []string{"C-HITL", "C-DSR-PROCESS"},
|
||||
Patterns: []string{"P-HITL-ENFORCED"},
|
||||
Condition: func(intake *UseCaseIntake) bool {
|
||||
return (intake.Outputs.LegalEffects || intake.Outputs.AccessDecisions || intake.Purpose.DecisionMaking) &&
|
||||
intake.Automation != AutomationAssistive
|
||||
},
|
||||
Rationale: func(intake *UseCaseIntake) string {
|
||||
return "Betroffene haben das Recht auf menschliche Überprüfung bei automatisierten Entscheidungen."
|
||||
},
|
||||
},
|
||||
{
|
||||
Code: "R-061",
|
||||
Category: "G. Transparenz",
|
||||
Title: "External Recommendations",
|
||||
TitleDE: "Externe Empfehlungen",
|
||||
Description: "Recommendations to users need transparency",
|
||||
DescriptionDE: "Empfehlungen an Nutzer erfordern Transparenz",
|
||||
Severity: SeverityINFO,
|
||||
ScoreDelta: 5,
|
||||
GDPRRef: "Art. 13/14 DSGVO",
|
||||
Controls: []string{"C-TRANSPARENCY"},
|
||||
Patterns: []string{},
|
||||
Condition: func(intake *UseCaseIntake) bool {
|
||||
return intake.Outputs.RecommendationsToUsers && intake.DataTypes.PersonalData
|
||||
},
|
||||
Rationale: func(intake *UseCaseIntake) string {
|
||||
return "Personalisierte Empfehlungen erfordern Information der Nutzer über die KI-Verarbeitung."
|
||||
},
|
||||
},
|
||||
{
|
||||
Code: "R-062",
|
||||
Category: "G. Transparenz",
|
||||
Title: "Content Generation without Disclosure",
|
||||
TitleDE: "Inhaltsgenerierung ohne Offenlegung",
|
||||
Description: "AI-generated content should be disclosed",
|
||||
DescriptionDE: "KI-generierte Inhalte sollten offengelegt werden",
|
||||
Severity: SeverityINFO,
|
||||
ScoreDelta: 5,
|
||||
GDPRRef: "EU-AI-Act Art. 52",
|
||||
Controls: []string{"C-TRANSPARENCY"},
|
||||
Patterns: []string{},
|
||||
Condition: func(intake *UseCaseIntake) bool {
|
||||
return intake.Outputs.ContentGeneration
|
||||
},
|
||||
Rationale: func(intake *UseCaseIntake) string {
|
||||
return "KI-generierte Inhalte sollten als solche gekennzeichnet werden (EU-AI-Act Transparenzpflicht)."
|
||||
},
|
||||
},
|
||||
// =========================================================================
|
||||
// H. Domain-spezifisch (R-070 bis R-074)
|
||||
// =========================================================================
|
||||
{
|
||||
Code: "R-070",
|
||||
Category: "H. Domain-spezifisch",
|
||||
Title: "Education + Scoring = Blocked",
|
||||
TitleDE: "Bildung + Scoring = Blockiert",
|
||||
Description: "Automated scoring of students is prohibited",
|
||||
DescriptionDE: "Automatisches Scoring von Schülern ist verboten",
|
||||
Severity: SeverityBLOCK,
|
||||
ScoreDelta: 50,
|
||||
GDPRRef: "Art. 8, Art. 22 DSGVO",
|
||||
Controls: []string{},
|
||||
Patterns: []string{},
|
||||
Condition: func(intake *UseCaseIntake) bool {
|
||||
return intake.Domain == DomainEducation &&
|
||||
intake.DataTypes.MinorData &&
|
||||
(intake.Purpose.EvaluationScoring || intake.Outputs.RankingsOrScores)
|
||||
},
|
||||
Rationale: func(intake *UseCaseIntake) string {
|
||||
return "Automatisches Scoring oder Ranking von Schülern/Minderjährigen ist aufgrund des besonderen Schutzes unzulässig."
|
||||
},
|
||||
},
|
||||
{
|
||||
Code: "R-071",
|
||||
Category: "H. Domain-spezifisch",
|
||||
Title: "Healthcare + Automated Diagnosis",
|
||||
TitleDE: "Gesundheit + Automatische Diagnose",
|
||||
Description: "Automated medical decisions require strict controls",
|
||||
DescriptionDE: "Automatische medizinische Entscheidungen erfordern strenge Kontrollen",
|
||||
Severity: SeverityBLOCK,
|
||||
ScoreDelta: 45,
|
||||
GDPRRef: "Art. 9, Art. 22 DSGVO",
|
||||
Controls: []string{"C-HITL", "C-DSFA", "C-ART9-BASIS"},
|
||||
Patterns: []string{"P-HITL-ENFORCED"},
|
||||
Condition: func(intake *UseCaseIntake) bool {
|
||||
return intake.Domain == DomainHealthcare &&
|
||||
intake.Automation == AutomationFullyAutomated &&
|
||||
intake.Purpose.DecisionMaking
|
||||
},
|
||||
Rationale: func(intake *UseCaseIntake) string {
|
||||
return "Vollautomatisierte medizinische Diagnosen oder Behandlungsentscheidungen sind ohne ärztliche Überprüfung unzulässig."
|
||||
},
|
||||
},
|
||||
{
|
||||
Code: "R-072",
|
||||
Category: "H. Domain-spezifisch",
|
||||
Title: "Finance + Automated Credit Scoring",
|
||||
TitleDE: "Finanzen + Automatisches Credit-Scoring",
|
||||
Description: "Automated credit decisions require transparency",
|
||||
DescriptionDE: "Automatische Kreditentscheidungen erfordern Transparenz",
|
||||
Severity: SeverityWARN,
|
||||
ScoreDelta: 20,
|
||||
GDPRRef: "Art. 22 DSGVO",
|
||||
Controls: []string{"C-HITL", "C-TRANSPARENCY", "C-DSR-PROCESS"},
|
||||
Patterns: []string{"P-HITL-ENFORCED"},
|
||||
Condition: func(intake *UseCaseIntake) bool {
|
||||
return intake.Domain == DomainFinance &&
|
||||
(intake.Purpose.EvaluationScoring || intake.Outputs.RankingsOrScores) &&
|
||||
intake.DataTypes.FinancialData
|
||||
},
|
||||
Rationale: func(intake *UseCaseIntake) string {
|
||||
return "Automatische Kreditwürdigkeitsprüfung erfordert Erklärbarkeit und Widerspruchsmöglichkeit."
|
||||
},
|
||||
},
|
||||
{
|
||||
Code: "R-073",
|
||||
Category: "H. Domain-spezifisch",
|
||||
Title: "Utilities + RAG Chatbot = Low Risk",
|
||||
TitleDE: "Versorgungsunternehmen + RAG-Chatbot = Niedriges Risiko",
|
||||
Description: "RAG-based customer service chatbot is low risk",
|
||||
DescriptionDE: "RAG-basierter Kundenservice-Chatbot ist risikoarm",
|
||||
Severity: SeverityINFO,
|
||||
ScoreDelta: 0,
|
||||
GDPRRef: "",
|
||||
Controls: []string{},
|
||||
Patterns: []string{"P-RAG-ONLY"},
|
||||
Condition: func(intake *UseCaseIntake) bool {
|
||||
return intake.Domain == DomainUtilities &&
|
||||
intake.ModelUsage.RAG &&
|
||||
intake.Purpose.CustomerSupport &&
|
||||
!intake.DataTypes.PersonalData
|
||||
},
|
||||
Rationale: func(intake *UseCaseIntake) string {
|
||||
return "Ein RAG-basierter Kundenservice-Chatbot ohne Speicherung personenbezogener Daten ist ein Best-Practice-Beispiel."
|
||||
},
|
||||
},
|
||||
{
|
||||
Code: "R-074",
|
||||
Category: "H. Domain-spezifisch",
|
||||
Title: "Public Sector + Automated Decisions",
|
||||
TitleDE: "Öffentlicher Sektor + Automatische Entscheidungen",
|
||||
Description: "Public sector automated decisions need special care",
|
||||
DescriptionDE: "Automatische Entscheidungen im öffentlichen Sektor erfordern besondere Sorgfalt",
|
||||
Severity: SeverityWARN,
|
||||
ScoreDelta: 20,
|
||||
GDPRRef: "Art. 22 DSGVO",
|
||||
Controls: []string{"C-HITL", "C-TRANSPARENCY", "C-DSFA"},
|
||||
Patterns: []string{"P-HITL-ENFORCED"},
|
||||
Condition: func(intake *UseCaseIntake) bool {
|
||||
return intake.Domain == DomainPublic &&
|
||||
intake.Purpose.DecisionMaking &&
|
||||
intake.Automation != AutomationAssistive
|
||||
},
|
||||
Rationale: func(intake *UseCaseIntake) string {
|
||||
return "Verwaltungsentscheidungen, die Bürger betreffen, erfordern besondere Transparenz und Überprüfungsmöglichkeiten."
|
||||
},
|
||||
},
|
||||
// =========================================================================
|
||||
// I. Aggregation (R-090 bis R-092) - Implicit in Evaluate()
|
||||
// =========================================================================
|
||||
{
|
||||
Code: "R-090",
|
||||
Category: "I. Aggregation",
|
||||
Title: "Block Rules Triggered",
|
||||
TitleDE: "Blockierungsregeln ausgelöst",
|
||||
Description: "Any BLOCK severity results in NO feasibility",
|
||||
DescriptionDE: "Jede BLOCK-Schwere führt zu NEIN-Machbarkeit",
|
||||
Severity: SeverityBLOCK,
|
||||
ScoreDelta: 0,
|
||||
GDPRRef: "",
|
||||
Controls: []string{},
|
||||
Patterns: []string{},
|
||||
Condition: func(intake *UseCaseIntake) bool {
|
||||
return false // handled in aggregation logic
|
||||
},
|
||||
Rationale: func(intake *UseCaseIntake) string {
|
||||
return "Eine oder mehrere kritische Regelverletzungen führen zur Einstufung als nicht umsetzbar."
|
||||
},
|
||||
},
|
||||
{
|
||||
Code: "R-091",
|
||||
Category: "I. Aggregation",
|
||||
Title: "Warning Rules Only",
|
||||
TitleDE: "Nur Warnungsregeln",
|
||||
Description: "Only WARN severity results in CONDITIONAL",
|
||||
DescriptionDE: "Nur WARN-Schwere führt zu BEDINGT",
|
||||
Severity: SeverityWARN,
|
||||
ScoreDelta: 0,
|
||||
GDPRRef: "",
|
||||
Controls: []string{},
|
||||
Patterns: []string{},
|
||||
Condition: func(intake *UseCaseIntake) bool {
|
||||
return false // handled in aggregation logic
|
||||
},
|
||||
Rationale: func(intake *UseCaseIntake) string {
|
||||
return "Warnungen erfordern Maßnahmen, blockieren aber nicht die Umsetzung."
|
||||
},
|
||||
},
|
||||
{
|
||||
Code: "R-092",
|
||||
Category: "I. Aggregation",
|
||||
Title: "Info Only - Clear Path",
|
||||
TitleDE: "Nur Info - Freier Weg",
|
||||
Description: "Only INFO severity results in YES",
|
||||
DescriptionDE: "Nur INFO-Schwere führt zu JA",
|
||||
Severity: SeverityINFO,
|
||||
ScoreDelta: 0,
|
||||
GDPRRef: "",
|
||||
Controls: []string{},
|
||||
Patterns: []string{},
|
||||
Condition: func(intake *UseCaseIntake) bool {
|
||||
return false // handled in aggregation logic
|
||||
},
|
||||
Rationale: func(intake *UseCaseIntake) string {
|
||||
return "Keine kritischen oder warnenden Regeln ausgelöst - Umsetzung empfohlen."
|
||||
},
|
||||
},
|
||||
// =========================================================================
|
||||
// J. Erklärung (R-100)
|
||||
// =========================================================================
|
||||
{
|
||||
Code: "R-100",
|
||||
Category: "J. Erklärung",
|
||||
Title: "Rejection Must Include Reason and Alternative",
|
||||
TitleDE: "Ablehnung muss Begründung und Alternative enthalten",
|
||||
Description: "When feasibility is NO, provide reason and alternative",
|
||||
DescriptionDE: "Bei Machbarkeit NEIN, Begründung und Alternative angeben",
|
||||
Severity: SeverityINFO,
|
||||
ScoreDelta: 0,
|
||||
GDPRRef: "",
|
||||
Controls: []string{},
|
||||
Patterns: []string{},
|
||||
Condition: func(intake *UseCaseIntake) bool {
|
||||
return false // handled in summary generation
|
||||
},
|
||||
Rationale: func(intake *UseCaseIntake) string {
|
||||
return "Jede Ablehnung enthält eine klare Begründung und einen alternativen Ansatz."
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user