package handlers import ( "encoding/json" "fmt" "net/http" "strings" "github.com/breakpilot/ai-compliance-sdk/internal/iace" "github.com/breakpilot/ai-compliance-sdk/internal/rbac" "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 } // NewIACEHandler creates a new IACEHandler with all required dependencies. func NewIACEHandler(store *iace.Store) *IACEHandler { return &IACEHandler{ store: store, engine: iace.NewRiskEngine(), classifier: iace.NewClassifier(), checker: iace.NewCompletenessChecker(), } } // ============================================================================ // 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) } // ============================================================================ // Project Management // ============================================================================ // CreateProject handles POST /projects // Creates a new IACE compliance project for a machine or system. func (h *IACEHandler) CreateProject(c *gin.Context) { tenantID, err := getTenantID(c) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } var req iace.CreateProjectRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } project, err := h.store.CreateProject(c.Request.Context(), tenantID, req) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusCreated, gin.H{"project": project}) } // ListProjects handles GET /projects // Lists all IACE projects for the authenticated tenant. func (h *IACEHandler) ListProjects(c *gin.Context) { tenantID, err := getTenantID(c) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } projects, err := h.store.ListProjects(c.Request.Context(), tenantID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } if projects == nil { projects = []iace.Project{} } c.JSON(http.StatusOK, iace.ProjectListResponse{ Projects: projects, Total: len(projects), }) } // GetProject handles GET /projects/:id // Returns a project with its components, classifications, and completeness gates. func (h *IACEHandler) GetProject(c *gin.Context) { projectID, err := uuid.Parse(c.Param("id")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project ID"}) return } project, err := h.store.GetProject(c.Request.Context(), projectID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } if project == nil { c.JSON(http.StatusNotFound, gin.H{"error": "project not found"}) return } components, _ := h.store.ListComponents(c.Request.Context(), projectID) classifications, _ := h.store.GetClassifications(c.Request.Context(), projectID) if components == nil { components = []iace.Component{} } if classifications == nil { classifications = []iace.RegulatoryClassification{} } // Build completeness context to compute gates ctx := h.buildCompletenessContext(c, project, components, classifications) result := h.checker.Check(ctx) c.JSON(http.StatusOK, iace.ProjectDetailResponse{ Project: *project, Components: components, Classifications: classifications, CompletenessGates: result.Gates, }) } // UpdateProject handles PUT /projects/:id // Partially updates a project's mutable fields. func (h *IACEHandler) UpdateProject(c *gin.Context) { projectID, err := uuid.Parse(c.Param("id")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project ID"}) return } var req iace.UpdateProjectRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } project, err := h.store.UpdateProject(c.Request.Context(), projectID, req) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } if project == nil { c.JSON(http.StatusNotFound, gin.H{"error": "project not found"}) return } c.JSON(http.StatusOK, gin.H{"project": project}) } // ArchiveProject handles DELETE /projects/:id // Archives a project by setting its status to archived. func (h *IACEHandler) ArchiveProject(c *gin.Context) { projectID, err := uuid.Parse(c.Param("id")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project ID"}) return } if err := h.store.ArchiveProject(c.Request.Context(), projectID); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"message": "project archived"}) } // ============================================================================ // Onboarding // ============================================================================ // InitFromProfile handles POST /projects/:id/init-from-profile // Initializes a project from a company profile and compliance scope JSON payload. func (h *IACEHandler) InitFromProfile(c *gin.Context) { projectID, err := uuid.Parse(c.Param("id")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project ID"}) return } project, err := h.store.GetProject(c.Request.Context(), projectID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } if project == nil { c.JSON(http.StatusNotFound, gin.H{"error": "project not found"}) return } var req iace.InitFromProfileRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } // Store the profile and scope in project metadata profileData := map[string]json.RawMessage{ "company_profile": req.CompanyProfile, "compliance_scope": req.ComplianceScope, } metadataBytes, _ := json.Marshal(profileData) metadataRaw := json.RawMessage(metadataBytes) updateReq := iace.UpdateProjectRequest{ Metadata: &metadataRaw, } project, err = h.store.UpdateProject(c.Request.Context(), projectID, updateReq) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } // Advance project status to onboarding if err := h.store.UpdateProjectStatus(c.Request.Context(), projectID, iace.ProjectStatusOnboarding); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } // Add audit trail entry userID := rbac.GetUserID(c) h.store.AddAuditEntry( c.Request.Context(), projectID, "project", projectID, iace.AuditActionUpdate, userID.String(), nil, metadataBytes, ) c.JSON(http.StatusOK, gin.H{ "message": "project initialized from profile", "project": project, }) } // CreateComponent handles POST /projects/:id/components // Adds a new component to a project. func (h *IACEHandler) CreateComponent(c *gin.Context) { projectID, err := uuid.Parse(c.Param("id")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project ID"}) return } var req iace.CreateComponentRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } // Override project ID from URL path req.ProjectID = projectID component, err := h.store.CreateComponent(c.Request.Context(), req) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } // Audit trail userID := rbac.GetUserID(c) newVals, _ := json.Marshal(component) h.store.AddAuditEntry( c.Request.Context(), projectID, "component", component.ID, iace.AuditActionCreate, userID.String(), nil, newVals, ) c.JSON(http.StatusCreated, gin.H{"component": component}) } // ListComponents handles GET /projects/:id/components // Lists all components for a project. func (h *IACEHandler) ListComponents(c *gin.Context) { projectID, err := uuid.Parse(c.Param("id")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project ID"}) return } components, err := h.store.ListComponents(c.Request.Context(), projectID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } if components == nil { components = []iace.Component{} } c.JSON(http.StatusOK, gin.H{ "components": components, "total": len(components), }) } // UpdateComponent handles PUT /projects/:id/components/:cid // Updates a component with the provided fields. func (h *IACEHandler) UpdateComponent(c *gin.Context) { _, err := uuid.Parse(c.Param("id")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project ID"}) return } componentID, err := uuid.Parse(c.Param("cid")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid component ID"}) return } var updates map[string]interface{} if err := c.ShouldBindJSON(&updates); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } component, err := h.store.UpdateComponent(c.Request.Context(), componentID, updates) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } if component == nil { c.JSON(http.StatusNotFound, gin.H{"error": "component not found"}) return } c.JSON(http.StatusOK, gin.H{"component": component}) } // DeleteComponent handles DELETE /projects/:id/components/:cid // Deletes a component from a project. func (h *IACEHandler) DeleteComponent(c *gin.Context) { _, err := uuid.Parse(c.Param("id")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project ID"}) return } componentID, err := uuid.Parse(c.Param("cid")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid component ID"}) return } if err := h.store.DeleteComponent(c.Request.Context(), componentID); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"message": "component deleted"}) } // CheckCompleteness handles POST /projects/:id/completeness-check // Loads all project data, evaluates all 25 CE completeness gates, updates the // project's completeness score, and returns the result. func (h *IACEHandler) CheckCompleteness(c *gin.Context) { projectID, err := uuid.Parse(c.Param("id")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project ID"}) return } project, err := h.store.GetProject(c.Request.Context(), projectID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } if project == nil { c.JSON(http.StatusNotFound, gin.H{"error": "project not found"}) return } // Load all related entities components, _ := h.store.ListComponents(c.Request.Context(), projectID) classifications, _ := h.store.GetClassifications(c.Request.Context(), projectID) hazards, _ := h.store.ListHazards(c.Request.Context(), projectID) // Collect all assessments and mitigations across all hazards 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) // Determine if the project has AI components hasAI := false for _, comp := range components { if comp.ComponentType == iace.ComponentTypeAIModel { hasAI = true break } } // Build completeness context completenessCtx := &iace.CompletenessContext{ Project: project, Components: components, Classifications: classifications, Hazards: hazards, Assessments: allAssessments, Mitigations: allMitigations, Evidence: evidence, TechFileSections: techFileSections, HasAI: hasAI, } // Run the checker result := h.checker.Check(completenessCtx) // Build risk summary for the project update riskSummary := map[string]int{ "total_hazards": len(hazards), } for _, a := range allAssessments { riskSummary[string(a.RiskLevel)]++ } // Update project completeness score and risk summary if err := h.store.UpdateProjectCompleteness( c.Request.Context(), projectID, result.Score, riskSummary, ); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{ "completeness": result, }) } // ============================================================================ // Classification // ============================================================================ // Classify handles POST /projects/:id/classify // Runs all regulatory classifiers (AI Act, Machinery Regulation, CRA, NIS2), // upserts each result into the store, and returns classifications. func (h *IACEHandler) Classify(c *gin.Context) { projectID, err := uuid.Parse(c.Param("id")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project ID"}) return } project, err := h.store.GetProject(c.Request.Context(), projectID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } if project == nil { c.JSON(http.StatusNotFound, gin.H{"error": "project not found"}) return } components, err := h.store.ListComponents(c.Request.Context(), projectID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } // Run all classifiers results := h.classifier.ClassifyAll(project, components) // Upsert each classification result into the store var classifications []iace.RegulatoryClassification for _, r := range results { reqsJSON, _ := json.Marshal(r.Requirements) classification, err := h.store.UpsertClassification( c.Request.Context(), projectID, r.Regulation, r.ClassificationResult, r.RiskLevel, r.Confidence, r.Reasoning, nil, // ragSources - not available from rule-based classifier reqsJSON, ) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } if classification != nil { classifications = append(classifications, *classification) } } // Advance project status to classification h.store.UpdateProjectStatus(c.Request.Context(), projectID, iace.ProjectStatusClassification) // Audit trail userID := rbac.GetUserID(c) newVals, _ := json.Marshal(classifications) h.store.AddAuditEntry( c.Request.Context(), projectID, "classification", projectID, iace.AuditActionCreate, userID.String(), nil, newVals, ) c.JSON(http.StatusOK, gin.H{ "classifications": classifications, "total": len(classifications), }) } // GetClassifications handles GET /projects/:id/classifications // Returns all regulatory classifications for a project. func (h *IACEHandler) GetClassifications(c *gin.Context) { projectID, err := uuid.Parse(c.Param("id")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project ID"}) return } classifications, err := h.store.GetClassifications(c.Request.Context(), projectID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } if classifications == nil { classifications = []iace.RegulatoryClassification{} } c.JSON(http.StatusOK, gin.H{ "classifications": classifications, "total": len(classifications), }) } // ClassifySingle handles POST /projects/:id/classify/:regulation // Runs a single regulatory classifier for the specified regulation type. func (h *IACEHandler) ClassifySingle(c *gin.Context) { projectID, err := uuid.Parse(c.Param("id")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project ID"}) return } regulation := iace.RegulationType(c.Param("regulation")) project, err := h.store.GetProject(c.Request.Context(), projectID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } if project == nil { c.JSON(http.StatusNotFound, gin.H{"error": "project not found"}) return } components, err := h.store.ListComponents(c.Request.Context(), projectID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } // Run the appropriate classifier var result iace.ClassificationResult switch regulation { case iace.RegulationAIAct: result = h.classifier.ClassifyAIAct(project, components) case iace.RegulationMachineryRegulation: result = h.classifier.ClassifyMachineryRegulation(project, components) case iace.RegulationCRA: result = h.classifier.ClassifyCRA(project, components) case iace.RegulationNIS2: result = h.classifier.ClassifyNIS2(project, components) default: c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("unknown regulation type: %s", regulation)}) return } // Upsert the classification result reqsJSON, _ := json.Marshal(result.Requirements) classification, err := h.store.UpsertClassification( c.Request.Context(), projectID, result.Regulation, result.ClassificationResult, result.RiskLevel, result.Confidence, result.Reasoning, nil, reqsJSON, ) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"classification": classification}) } // ============================================================================ // Hazard & Risk // ============================================================================ // ListHazardLibrary handles GET /hazard-library // Returns built-in hazard library entries merged with any custom DB entries, // optionally filtered by ?category and ?componentType. func (h *IACEHandler) ListHazardLibrary(c *gin.Context) { category := c.Query("category") componentType := c.Query("componentType") // Start with built-in templates from Go code builtinEntries := iace.GetBuiltinHazardLibrary() // Apply filters to built-in entries var entries []iace.HazardLibraryEntry for _, entry := range builtinEntries { if category != "" && entry.Category != category { continue } if componentType != "" && !containsString(entry.ApplicableComponentTypes, componentType) { continue } entries = append(entries, entry) } // Merge with custom DB entries (tenant-specific) dbEntries, err := h.store.ListHazardLibrary(c.Request.Context(), category, componentType) if err == nil && len(dbEntries) > 0 { // Add DB entries that are not built-in (avoid duplicates) builtinIDs := make(map[string]bool) for _, e := range entries { builtinIDs[e.ID.String()] = true } for _, dbEntry := range dbEntries { if !builtinIDs[dbEntry.ID.String()] { entries = append(entries, dbEntry) } } } if entries == nil { entries = []iace.HazardLibraryEntry{} } c.JSON(http.StatusOK, gin.H{ "hazard_library": entries, "total": len(entries), }) } // ListControlsLibrary handles GET /controls-library // Returns the built-in controls library, optionally filtered by ?domain and ?category. func (h *IACEHandler) ListControlsLibrary(c *gin.Context) { domain := c.Query("domain") category := c.Query("category") all := iace.GetControlsLibrary() var filtered []iace.ControlLibraryEntry for _, entry := range all { if domain != "" && entry.Domain != domain { continue } if category != "" && !containsString(entry.MapsToHazardCategories, category) { continue } filtered = append(filtered, entry) } if filtered == nil { filtered = []iace.ControlLibraryEntry{} } c.JSON(http.StatusOK, gin.H{ "controls": filtered, "total": len(filtered), }) } // 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 } // CreateHazard handles POST /projects/:id/hazards // Creates a new hazard within a project. func (h *IACEHandler) CreateHazard(c *gin.Context) { projectID, err := uuid.Parse(c.Param("id")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project ID"}) return } var req iace.CreateHazardRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } // Override project ID from URL path req.ProjectID = projectID hazard, err := h.store.CreateHazard(c.Request.Context(), req) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } // Audit trail userID := rbac.GetUserID(c) newVals, _ := json.Marshal(hazard) h.store.AddAuditEntry( c.Request.Context(), projectID, "hazard", hazard.ID, iace.AuditActionCreate, userID.String(), nil, newVals, ) c.JSON(http.StatusCreated, gin.H{"hazard": hazard}) } // ListHazards handles GET /projects/:id/hazards // Lists all hazards for a project. func (h *IACEHandler) ListHazards(c *gin.Context) { projectID, err := uuid.Parse(c.Param("id")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project ID"}) return } hazards, err := h.store.ListHazards(c.Request.Context(), projectID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } if hazards == nil { hazards = []iace.Hazard{} } c.JSON(http.StatusOK, gin.H{ "hazards": hazards, "total": len(hazards), }) } // UpdateHazard handles PUT /projects/:id/hazards/:hid // Updates a hazard with the provided fields. func (h *IACEHandler) UpdateHazard(c *gin.Context) { _, err := uuid.Parse(c.Param("id")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project ID"}) return } hazardID, err := uuid.Parse(c.Param("hid")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid hazard ID"}) return } var updates map[string]interface{} if err := c.ShouldBindJSON(&updates); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } hazard, err := h.store.UpdateHazard(c.Request.Context(), hazardID, updates) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } if hazard == nil { c.JSON(http.StatusNotFound, gin.H{"error": "hazard not found"}) return } c.JSON(http.StatusOK, gin.H{"hazard": hazard}) } // SuggestHazards handles POST /projects/:id/hazards/suggest // Returns hazard library matches based on the project's components. // TODO: Enhance with LLM-based suggestions for more intelligent matching. func (h *IACEHandler) SuggestHazards(c *gin.Context) { projectID, err := uuid.Parse(c.Param("id")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project ID"}) return } components, err := h.store.ListComponents(c.Request.Context(), projectID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } // Collect unique component types from the project componentTypes := make(map[string]bool) for _, comp := range components { componentTypes[string(comp.ComponentType)] = true } // Match built-in hazard templates against project component types var suggestions []iace.HazardLibraryEntry seen := make(map[uuid.UUID]bool) builtinEntries := iace.GetBuiltinHazardLibrary() for _, entry := range builtinEntries { for _, applicableType := range entry.ApplicableComponentTypes { if componentTypes[applicableType] && !seen[entry.ID] { seen[entry.ID] = true suggestions = append(suggestions, entry) break } } } // Also check DB for custom tenant-specific hazard templates for compType := range componentTypes { dbEntries, err := h.store.ListHazardLibrary(c.Request.Context(), "", compType) if err != nil { continue } for _, entry := range dbEntries { if !seen[entry.ID] { seen[entry.ID] = true suggestions = append(suggestions, entry) } } } if suggestions == nil { suggestions = []iace.HazardLibraryEntry{} } c.JSON(http.StatusOK, gin.H{ "suggestions": suggestions, "total": len(suggestions), "component_types": componentTypeKeys(componentTypes), "_note": "TODO: LLM-based suggestion ranking not yet implemented", }) } // AssessRisk handles POST /projects/:id/hazards/:hid/assess // Performs a quantitative risk assessment for a hazard using the IACE risk engine. func (h *IACEHandler) AssessRisk(c *gin.Context) { projectID, err := uuid.Parse(c.Param("id")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project ID"}) return } hazardID, err := uuid.Parse(c.Param("hid")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid hazard ID"}) return } // Verify hazard exists hazard, err := h.store.GetHazard(c.Request.Context(), hazardID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } if hazard == nil { c.JSON(http.StatusNotFound, gin.H{"error": "hazard not found"}) return } var req iace.AssessRiskRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } // Override hazard ID from URL path req.HazardID = hazardID userID := rbac.GetUserID(c) // Calculate risk using the engine inherentRisk := h.engine.CalculateInherentRisk(req.Severity, req.Exposure, req.Probability, req.Avoidance) controlEff := h.engine.CalculateControlEffectiveness(req.ControlMaturity, req.ControlCoverage, req.TestEvidenceStrength) residualRisk := h.engine.CalculateResidualRisk(req.Severity, req.Exposure, req.Probability, controlEff) riskLevel := h.engine.DetermineRiskLevel(residualRisk) acceptable, acceptanceReason := h.engine.IsAcceptable(residualRisk, false, req.AcceptanceJustification != "") // Determine version by checking existing assessments existingAssessments, _ := h.store.ListAssessments(c.Request.Context(), hazardID) version := len(existingAssessments) + 1 assessment := &iace.RiskAssessment{ HazardID: hazardID, Version: version, AssessmentType: iace.AssessmentTypeInitial, Severity: req.Severity, Exposure: req.Exposure, Probability: req.Probability, Avoidance: req.Avoidance, InherentRisk: inherentRisk, ControlMaturity: req.ControlMaturity, ControlCoverage: req.ControlCoverage, TestEvidenceStrength: req.TestEvidenceStrength, CEff: controlEff, ResidualRisk: residualRisk, RiskLevel: riskLevel, IsAcceptable: acceptable, AcceptanceJustification: req.AcceptanceJustification, AssessedBy: userID, } if err := h.store.CreateRiskAssessment(c.Request.Context(), assessment); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } // Update hazard status h.store.UpdateHazard(c.Request.Context(), hazardID, map[string]interface{}{ "status": string(iace.HazardStatusAssessed), }) // Audit trail newVals, _ := json.Marshal(assessment) h.store.AddAuditEntry( c.Request.Context(), projectID, "risk_assessment", assessment.ID, iace.AuditActionCreate, userID.String(), nil, newVals, ) c.JSON(http.StatusCreated, gin.H{ "assessment": assessment, "acceptable": acceptable, "acceptance_reason": acceptanceReason, }) } // GetRiskSummary handles GET /projects/:id/risk-summary // Returns an aggregated risk overview for a project. func (h *IACEHandler) GetRiskSummary(c *gin.Context) { projectID, err := uuid.Parse(c.Param("id")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project ID"}) return } summary, err := h.store.GetRiskSummary(c.Request.Context(), projectID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"risk_summary": summary}) } // CreateMitigation handles POST /projects/:id/hazards/:hid/mitigations // Creates a new mitigation measure for a hazard. func (h *IACEHandler) CreateMitigation(c *gin.Context) { projectID, err := uuid.Parse(c.Param("id")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project ID"}) return } hazardID, err := uuid.Parse(c.Param("hid")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid hazard ID"}) return } var req iace.CreateMitigationRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } // Override hazard ID from URL path req.HazardID = hazardID mitigation, err := h.store.CreateMitigation(c.Request.Context(), req) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } // Update hazard status to mitigated h.store.UpdateHazard(c.Request.Context(), hazardID, map[string]interface{}{ "status": string(iace.HazardStatusMitigated), }) // Audit trail userID := rbac.GetUserID(c) newVals, _ := json.Marshal(mitigation) h.store.AddAuditEntry( c.Request.Context(), projectID, "mitigation", mitigation.ID, iace.AuditActionCreate, userID.String(), nil, newVals, ) c.JSON(http.StatusCreated, gin.H{"mitigation": mitigation}) } // UpdateMitigation handles PUT /mitigations/:mid // Updates a mitigation measure with the provided fields. func (h *IACEHandler) UpdateMitigation(c *gin.Context) { mitigationID, err := uuid.Parse(c.Param("mid")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid mitigation ID"}) return } var updates map[string]interface{} if err := c.ShouldBindJSON(&updates); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } mitigation, err := h.store.UpdateMitigation(c.Request.Context(), mitigationID, updates) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } if mitigation == nil { c.JSON(http.StatusNotFound, gin.H{"error": "mitigation not found"}) return } c.JSON(http.StatusOK, gin.H{"mitigation": mitigation}) } // VerifyMitigation handles POST /mitigations/:mid/verify // Marks a mitigation as verified with a verification result. func (h *IACEHandler) VerifyMitigation(c *gin.Context) { mitigationID, err := uuid.Parse(c.Param("mid")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid mitigation ID"}) return } var req struct { VerificationResult string `json:"verification_result" binding:"required"` } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } userID := rbac.GetUserID(c) if err := h.store.VerifyMitigation( c.Request.Context(), mitigationID, req.VerificationResult, userID.String(), ); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"message": "mitigation verified"}) } // ReassessRisk handles POST /projects/:id/hazards/:hid/reassess // Creates a post-mitigation risk reassessment for a hazard. func (h *IACEHandler) ReassessRisk(c *gin.Context) { projectID, err := uuid.Parse(c.Param("id")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project ID"}) return } hazardID, err := uuid.Parse(c.Param("hid")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid hazard ID"}) return } // Verify hazard exists hazard, err := h.store.GetHazard(c.Request.Context(), hazardID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } if hazard == nil { c.JSON(http.StatusNotFound, gin.H{"error": "hazard not found"}) return } var req iace.AssessRiskRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } userID := rbac.GetUserID(c) // Calculate risk using the engine inherentRisk := h.engine.CalculateInherentRisk(req.Severity, req.Exposure, req.Probability, req.Avoidance) controlEff := h.engine.CalculateControlEffectiveness(req.ControlMaturity, req.ControlCoverage, req.TestEvidenceStrength) residualRisk := h.engine.CalculateResidualRisk(req.Severity, req.Exposure, req.Probability, controlEff) riskLevel := h.engine.DetermineRiskLevel(residualRisk) // For reassessment, check if all reduction steps have been applied mitigations, _ := h.store.ListMitigations(c.Request.Context(), hazardID) allReductionStepsApplied := len(mitigations) > 0 for _, m := range mitigations { if m.Status != iace.MitigationStatusVerified { allReductionStepsApplied = false break } } acceptable, acceptanceReason := h.engine.IsAcceptable(residualRisk, allReductionStepsApplied, req.AcceptanceJustification != "") // Determine version existingAssessments, _ := h.store.ListAssessments(c.Request.Context(), hazardID) version := len(existingAssessments) + 1 assessment := &iace.RiskAssessment{ HazardID: hazardID, Version: version, AssessmentType: iace.AssessmentTypePostMitigation, Severity: req.Severity, Exposure: req.Exposure, Probability: req.Probability, Avoidance: req.Avoidance, InherentRisk: inherentRisk, ControlMaturity: req.ControlMaturity, ControlCoverage: req.ControlCoverage, TestEvidenceStrength: req.TestEvidenceStrength, CEff: controlEff, ResidualRisk: residualRisk, RiskLevel: riskLevel, IsAcceptable: acceptable, AcceptanceJustification: req.AcceptanceJustification, AssessedBy: userID, } if err := h.store.CreateRiskAssessment(c.Request.Context(), assessment); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } // Audit trail newVals, _ := json.Marshal(assessment) h.store.AddAuditEntry( c.Request.Context(), projectID, "risk_assessment", assessment.ID, iace.AuditActionCreate, userID.String(), nil, newVals, ) c.JSON(http.StatusCreated, gin.H{ "assessment": assessment, "acceptable": acceptable, "acceptance_reason": acceptanceReason, "all_reduction_steps_applied": allReductionStepsApplied, }) } // ============================================================================ // Evidence & Verification // ============================================================================ // UploadEvidence handles POST /projects/:id/evidence // Creates a new evidence record for a project. func (h *IACEHandler) UploadEvidence(c *gin.Context) { projectID, err := uuid.Parse(c.Param("id")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project ID"}) return } var req struct { MitigationID *uuid.UUID `json:"mitigation_id,omitempty"` VerificationPlanID *uuid.UUID `json:"verification_plan_id,omitempty"` FileName string `json:"file_name" binding:"required"` FilePath string `json:"file_path" binding:"required"` FileHash string `json:"file_hash" binding:"required"` FileSize int64 `json:"file_size" binding:"required"` MimeType string `json:"mime_type" binding:"required"` Description string `json:"description,omitempty"` } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } userID := rbac.GetUserID(c) evidence := &iace.Evidence{ ProjectID: projectID, MitigationID: req.MitigationID, VerificationPlanID: req.VerificationPlanID, FileName: req.FileName, FilePath: req.FilePath, FileHash: req.FileHash, FileSize: req.FileSize, MimeType: req.MimeType, Description: req.Description, UploadedBy: userID, } if err := h.store.CreateEvidence(c.Request.Context(), evidence); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } // Audit trail newVals, _ := json.Marshal(evidence) h.store.AddAuditEntry( c.Request.Context(), projectID, "evidence", evidence.ID, iace.AuditActionCreate, userID.String(), nil, newVals, ) c.JSON(http.StatusCreated, gin.H{"evidence": evidence}) } // ListEvidence handles GET /projects/:id/evidence // Lists all evidence records for a project. func (h *IACEHandler) ListEvidence(c *gin.Context) { projectID, err := uuid.Parse(c.Param("id")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project ID"}) return } evidence, err := h.store.ListEvidence(c.Request.Context(), projectID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } if evidence == nil { evidence = []iace.Evidence{} } c.JSON(http.StatusOK, gin.H{ "evidence": evidence, "total": len(evidence), }) } // CreateVerificationPlan handles POST /projects/:id/verification-plan // Creates a new verification plan for a project. func (h *IACEHandler) CreateVerificationPlan(c *gin.Context) { projectID, err := uuid.Parse(c.Param("id")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project ID"}) return } var req iace.CreateVerificationPlanRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } // Override project ID from URL path req.ProjectID = projectID plan, err := h.store.CreateVerificationPlan(c.Request.Context(), req) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } // Audit trail userID := rbac.GetUserID(c) newVals, _ := json.Marshal(plan) h.store.AddAuditEntry( c.Request.Context(), projectID, "verification_plan", plan.ID, iace.AuditActionCreate, userID.String(), nil, newVals, ) c.JSON(http.StatusCreated, gin.H{"verification_plan": plan}) } // UpdateVerificationPlan handles PUT /verification-plan/:vid // Updates a verification plan with the provided fields. func (h *IACEHandler) UpdateVerificationPlan(c *gin.Context) { planID, err := uuid.Parse(c.Param("vid")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid verification plan ID"}) return } var updates map[string]interface{} if err := c.ShouldBindJSON(&updates); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } plan, err := h.store.UpdateVerificationPlan(c.Request.Context(), planID, updates) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } if plan == nil { c.JSON(http.StatusNotFound, gin.H{"error": "verification plan not found"}) return } c.JSON(http.StatusOK, gin.H{"verification_plan": plan}) } // CompleteVerification handles POST /verification-plan/:vid/complete // Marks a verification plan as completed with a result. func (h *IACEHandler) CompleteVerification(c *gin.Context) { planID, err := uuid.Parse(c.Param("vid")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid verification plan ID"}) return } var req struct { Result string `json:"result" binding:"required"` } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } userID := rbac.GetUserID(c) if err := h.store.CompleteVerification( c.Request.Context(), planID, req.Result, userID.String(), ); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"message": "verification completed"}) } // ============================================================================ // CE Technical File // ============================================================================ // GenerateTechFile handles POST /projects/:id/tech-file/generate // Generates technical file sections for a project. // TODO: Integrate LLM for intelligent content generation based on project data. func (h *IACEHandler) GenerateTechFile(c *gin.Context) { projectID, err := uuid.Parse(c.Param("id")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project ID"}) return } project, err := h.store.GetProject(c.Request.Context(), projectID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } if project == nil { c.JSON(http.StatusNotFound, gin.H{"error": "project not found"}) return } // Define the standard CE technical file sections to generate sectionDefinitions := []struct { SectionType string Title string }{ {"general_description", "General Description of the Machinery"}, {"risk_assessment_report", "Risk Assessment Report"}, {"hazard_log_combined", "Combined Hazard Log"}, {"essential_requirements", "Essential Health and Safety Requirements"}, {"design_specifications", "Design Specifications and Drawings"}, {"test_reports", "Test Reports and Verification Results"}, {"standards_applied", "Applied Harmonised Standards"}, {"declaration_of_conformity", "EU Declaration of Conformity"}, } // Check if project has AI components for additional sections components, _ := h.store.ListComponents(c.Request.Context(), projectID) hasAI := false for _, comp := range components { if comp.ComponentType == iace.ComponentTypeAIModel { hasAI = true break } } if hasAI { sectionDefinitions = append(sectionDefinitions, struct { SectionType string Title string }{"ai_intended_purpose", "AI System Intended Purpose"}, struct { SectionType string Title string }{"ai_model_description", "AI Model Description and Training Data"}, struct { SectionType string Title string }{"ai_risk_management", "AI Risk Management System"}, struct { SectionType string Title string }{"ai_human_oversight", "AI Human Oversight Measures"}, ) } // Generate each section with placeholder content // TODO: Replace placeholder content with LLM-generated content based on project data var sections []iace.TechFileSection existingSections, _ := h.store.ListTechFileSections(c.Request.Context(), projectID) existingMap := make(map[string]bool) for _, s := range existingSections { existingMap[s.SectionType] = true } for _, def := range sectionDefinitions { // Skip sections that already exist if existingMap[def.SectionType] { continue } content := fmt.Sprintf( "[Auto-generated placeholder for '%s']\n\n"+ "Machine: %s\nManufacturer: %s\nType: %s\n\n"+ "TODO: Replace this placeholder with actual content. "+ "LLM-based generation will be integrated in a future release.", def.Title, project.MachineName, project.Manufacturer, project.MachineType, ) section, err := h.store.CreateTechFileSection( c.Request.Context(), projectID, def.SectionType, def.Title, content, ) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } sections = append(sections, *section) } // Update project status h.store.UpdateProjectStatus(c.Request.Context(), projectID, iace.ProjectStatusTechFile) // Audit trail userID := rbac.GetUserID(c) h.store.AddAuditEntry( c.Request.Context(), projectID, "tech_file", projectID, iace.AuditActionCreate, userID.String(), nil, nil, ) c.JSON(http.StatusCreated, gin.H{ "sections_created": len(sections), "sections": sections, "_note": "TODO: LLM-based content generation not yet implemented", }) } // ListTechFileSections handles GET /projects/:id/tech-file // Lists all technical file sections for a project. func (h *IACEHandler) ListTechFileSections(c *gin.Context) { projectID, err := uuid.Parse(c.Param("id")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project ID"}) return } sections, err := h.store.ListTechFileSections(c.Request.Context(), projectID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } if sections == nil { sections = []iace.TechFileSection{} } c.JSON(http.StatusOK, gin.H{ "sections": sections, "total": len(sections), }) } // UpdateTechFileSection handles PUT /projects/:id/tech-file/:section // Updates the content of a technical file section (identified by section_type). func (h *IACEHandler) UpdateTechFileSection(c *gin.Context) { projectID, err := uuid.Parse(c.Param("id")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project ID"}) return } sectionType := c.Param("section") if sectionType == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "section type required"}) return } var req struct { Content string `json:"content" binding:"required"` } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } // Find the section by project ID and section type sections, err := h.store.ListTechFileSections(c.Request.Context(), projectID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } var sectionID uuid.UUID found := false for _, s := range sections { if s.SectionType == sectionType { sectionID = s.ID found = true break } } if !found { c.JSON(http.StatusNotFound, gin.H{"error": fmt.Sprintf("tech file section '%s' not found", sectionType)}) return } if err := h.store.UpdateTechFileSection(c.Request.Context(), sectionID, req.Content); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } // Audit trail userID := rbac.GetUserID(c) h.store.AddAuditEntry( c.Request.Context(), projectID, "tech_file_section", sectionID, iace.AuditActionUpdate, userID.String(), nil, nil, ) c.JSON(http.StatusOK, gin.H{"message": "tech file section updated"}) } // ApproveTechFileSection handles POST /projects/:id/tech-file/:section/approve // Marks a technical file section as approved. func (h *IACEHandler) ApproveTechFileSection(c *gin.Context) { projectID, err := uuid.Parse(c.Param("id")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project ID"}) return } sectionType := c.Param("section") if sectionType == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "section type required"}) return } // Find the section by project ID and section type sections, err := h.store.ListTechFileSections(c.Request.Context(), projectID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } var sectionID uuid.UUID found := false for _, s := range sections { if s.SectionType == sectionType { sectionID = s.ID found = true break } } if !found { c.JSON(http.StatusNotFound, gin.H{"error": fmt.Sprintf("tech file section '%s' not found", sectionType)}) return } userID := rbac.GetUserID(c) if err := h.store.ApproveTechFileSection(c.Request.Context(), sectionID, userID.String()); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } // Audit trail h.store.AddAuditEntry( c.Request.Context(), projectID, "tech_file_section", sectionID, iace.AuditActionApprove, userID.String(), nil, nil, ) c.JSON(http.StatusOK, gin.H{"message": "tech file section approved"}) } // ExportTechFile handles GET /projects/:id/tech-file/export // Exports all tech file sections as a combined JSON document. // TODO: Implement PDF export with proper formatting. func (h *IACEHandler) ExportTechFile(c *gin.Context) { projectID, err := uuid.Parse(c.Param("id")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project ID"}) return } project, err := h.store.GetProject(c.Request.Context(), projectID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } if project == nil { c.JSON(http.StatusNotFound, gin.H{"error": "project not found"}) return } sections, err := h.store.ListTechFileSections(c.Request.Context(), projectID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } // Check if all sections are approved allApproved := true for _, s := range sections { if s.Status != iace.TechFileSectionStatusApproved { allApproved = false break } } classifications, _ := h.store.GetClassifications(c.Request.Context(), projectID) riskSummary, _ := h.store.GetRiskSummary(c.Request.Context(), projectID) c.JSON(http.StatusOK, gin.H{ "project": project, "sections": sections, "classifications": classifications, "risk_summary": riskSummary, "all_approved": allApproved, "export_format": "json", "_note": "PDF export will be available in a future release", }) } // ============================================================================ // Monitoring // ============================================================================ // CreateMonitoringEvent handles POST /projects/:id/monitoring // Creates a new post-market monitoring event. func (h *IACEHandler) CreateMonitoringEvent(c *gin.Context) { projectID, err := uuid.Parse(c.Param("id")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project ID"}) return } var req iace.CreateMonitoringEventRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } // Override project ID from URL path req.ProjectID = projectID event, err := h.store.CreateMonitoringEvent(c.Request.Context(), req) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } // Audit trail userID := rbac.GetUserID(c) newVals, _ := json.Marshal(event) h.store.AddAuditEntry( c.Request.Context(), projectID, "monitoring_event", event.ID, iace.AuditActionCreate, userID.String(), nil, newVals, ) c.JSON(http.StatusCreated, gin.H{"monitoring_event": event}) } // ListMonitoringEvents handles GET /projects/:id/monitoring // Lists all monitoring events for a project. func (h *IACEHandler) ListMonitoringEvents(c *gin.Context) { projectID, err := uuid.Parse(c.Param("id")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project ID"}) return } events, err := h.store.ListMonitoringEvents(c.Request.Context(), projectID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } if events == nil { events = []iace.MonitoringEvent{} } c.JSON(http.StatusOK, gin.H{ "monitoring_events": events, "total": len(events), }) } // UpdateMonitoringEvent handles PUT /projects/:id/monitoring/:eid // Updates a monitoring event with the provided fields. func (h *IACEHandler) UpdateMonitoringEvent(c *gin.Context) { _, err := uuid.Parse(c.Param("id")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project ID"}) return } eventID, err := uuid.Parse(c.Param("eid")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid monitoring event ID"}) return } var updates map[string]interface{} if err := c.ShouldBindJSON(&updates); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } event, err := h.store.UpdateMonitoringEvent(c.Request.Context(), eventID, updates) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } if event == nil { c.JSON(http.StatusNotFound, gin.H{"error": "monitoring event not found"}) return } c.JSON(http.StatusOK, gin.H{"monitoring_event": event}) } // GetAuditTrail handles GET /projects/:id/audit-trail // Returns all audit trail entries for a project, newest first. func (h *IACEHandler) GetAuditTrail(c *gin.Context) { projectID, err := uuid.Parse(c.Param("id")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project ID"}) return } entries, err := h.store.ListAuditTrail(c.Request.Context(), projectID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } if entries == nil { entries = []iace.AuditTrailEntry{} } c.JSON(http.StatusOK, gin.H{ "audit_trail": entries, "total": len(entries), }) } // ============================================================================ // 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, } } // 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] } } }