iace_handler.go (2706 LOC) split into 9 files: - iace_handler.go: struct, constructor, shared helpers (~156 LOC) - iace_handler_projects.go: project CRUD + InitFromProfile (~310 LOC) - iace_handler_components.go: components + classification (~387 LOC) - iace_handler_hazards.go: hazard library, CRUD, risk assessment (~469 LOC) - iace_handler_mitigations.go: mitigations, evidence, verification plans (~293 LOC) - iace_handler_techfile.go: CE tech file generation/export (~452 LOC) - iace_handler_monitoring.go: monitoring events + audit trail (~134 LOC) - iace_handler_refdata.go: ISO 12100 ref data, patterns, suggestions (~465 LOC) - iace_handler_rag.go: RAG library search + section enrichment (~142 LOC) training_handlers.go (1864 LOC) split into 9 files: - training_handlers.go: struct + constructor (~23 LOC) - training_handlers_modules.go: module CRUD (~226 LOC) - training_handlers_matrix.go: CTM matrix endpoints (~95 LOC) - training_handlers_assignments.go: assignment lifecycle (~243 LOC) - training_handlers_quiz.go: quiz submit/grade/attempts (~185 LOC) - training_handlers_content.go: LLM content/audio/video generation (~274 LOC) - training_handlers_media.go: media, streaming, interactive video (~325 LOC) - training_handlers_blocks.go: block configs + canonical controls (~280 LOC) - training_handlers_stats.go: deadlines, escalation, audit, certificates (~290 LOC) All files remain in package handlers. Zero behavior changes. All exported function names preserved. All files under 500 LOC hard cap. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
157 lines
4.8 KiB
Go
157 lines
4.8 KiB
Go
package handlers
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/breakpilot/ai-compliance-sdk/internal/iace"
|
|
"github.com/breakpilot/ai-compliance-sdk/internal/llm"
|
|
"github.com/breakpilot/ai-compliance-sdk/internal/rbac"
|
|
"github.com/breakpilot/ai-compliance-sdk/internal/ucca"
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
// ============================================================================
|
|
// Handler Struct & Constructor
|
|
// ============================================================================
|
|
|
|
// IACEHandler handles HTTP requests for the IACE module (Inherent-risk Adjusted
|
|
// Control Effectiveness). It provides endpoints for project management, component
|
|
// onboarding, regulatory classification, hazard/risk analysis, evidence management,
|
|
// CE technical file generation, and post-market monitoring.
|
|
type IACEHandler struct {
|
|
store *iace.Store
|
|
engine *iace.RiskEngine
|
|
classifier *iace.Classifier
|
|
checker *iace.CompletenessChecker
|
|
ragClient *ucca.LegalRAGClient
|
|
techFileGen *iace.TechFileGenerator
|
|
exporter *iace.DocumentExporter
|
|
}
|
|
|
|
// NewIACEHandler creates a new IACEHandler with all required dependencies.
|
|
func NewIACEHandler(store *iace.Store, providerRegistry *llm.ProviderRegistry) *IACEHandler {
|
|
ragClient := ucca.NewLegalRAGClient()
|
|
return &IACEHandler{
|
|
store: store,
|
|
engine: iace.NewRiskEngine(),
|
|
classifier: iace.NewClassifier(),
|
|
checker: iace.NewCompletenessChecker(),
|
|
ragClient: ragClient,
|
|
techFileGen: iace.NewTechFileGenerator(providerRegistry, ragClient, store),
|
|
exporter: iace.NewDocumentExporter(),
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// Helper: Tenant ID extraction
|
|
// ============================================================================
|
|
|
|
// getTenantID extracts the tenant UUID from the X-Tenant-Id header.
|
|
// It first checks the rbac middleware context; if not present, falls back to the
|
|
// raw header value.
|
|
func getTenantID(c *gin.Context) (uuid.UUID, error) {
|
|
// Prefer value set by RBAC middleware
|
|
tid := rbac.GetTenantID(c)
|
|
if tid != uuid.Nil {
|
|
return tid, nil
|
|
}
|
|
|
|
tenantStr := c.GetHeader("X-Tenant-Id")
|
|
if tenantStr == "" {
|
|
return uuid.Nil, fmt.Errorf("X-Tenant-Id header required")
|
|
}
|
|
return uuid.Parse(tenantStr)
|
|
}
|
|
|
|
// ============================================================================
|
|
// Internal Helpers
|
|
// ============================================================================
|
|
|
|
// buildCompletenessContext constructs the CompletenessContext needed by the checker
|
|
// by loading all related entities for a project.
|
|
func (h *IACEHandler) buildCompletenessContext(
|
|
c *gin.Context,
|
|
project *iace.Project,
|
|
components []iace.Component,
|
|
classifications []iace.RegulatoryClassification,
|
|
) *iace.CompletenessContext {
|
|
projectID := project.ID
|
|
|
|
hazards, _ := h.store.ListHazards(c.Request.Context(), projectID)
|
|
|
|
var allAssessments []iace.RiskAssessment
|
|
var allMitigations []iace.Mitigation
|
|
for _, hazard := range hazards {
|
|
assessments, _ := h.store.ListAssessments(c.Request.Context(), hazard.ID)
|
|
allAssessments = append(allAssessments, assessments...)
|
|
|
|
mitigations, _ := h.store.ListMitigations(c.Request.Context(), hazard.ID)
|
|
allMitigations = append(allMitigations, mitigations...)
|
|
}
|
|
|
|
evidence, _ := h.store.ListEvidence(c.Request.Context(), projectID)
|
|
techFileSections, _ := h.store.ListTechFileSections(c.Request.Context(), projectID)
|
|
|
|
hasAI := false
|
|
for _, comp := range components {
|
|
if comp.ComponentType == iace.ComponentTypeAIModel {
|
|
hasAI = true
|
|
break
|
|
}
|
|
}
|
|
|
|
return &iace.CompletenessContext{
|
|
Project: project,
|
|
Components: components,
|
|
Classifications: classifications,
|
|
Hazards: hazards,
|
|
Assessments: allAssessments,
|
|
Mitigations: allMitigations,
|
|
Evidence: evidence,
|
|
TechFileSections: techFileSections,
|
|
HasAI: hasAI,
|
|
}
|
|
}
|
|
|
|
// containsString checks if a string slice contains the given value.
|
|
func containsString(slice []string, val string) bool {
|
|
for _, s := range slice {
|
|
if s == val {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// componentTypeKeys extracts keys from a map[string]bool and returns them as a sorted slice.
|
|
func componentTypeKeys(m map[string]bool) []string {
|
|
keys := make([]string, 0, len(m))
|
|
for k := range m {
|
|
keys = append(keys, k)
|
|
}
|
|
// Sort for deterministic output
|
|
sortStrings(keys)
|
|
return keys
|
|
}
|
|
|
|
// sortStrings sorts a slice of strings in place using a simple insertion sort.
|
|
func sortStrings(s []string) {
|
|
for i := 1; i < len(s); i++ {
|
|
for j := i; j > 0 && strings.Compare(s[j-1], s[j]) > 0; j-- {
|
|
s[j-1], s[j] = s[j], s[j-1]
|
|
}
|
|
}
|
|
}
|
|
|
|
// mustMarshalJSON marshals the given value to json.RawMessage.
|
|
func mustMarshalJSON(v interface{}) json.RawMessage {
|
|
data, err := json.Marshal(v)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
return data
|
|
}
|