package handlers import ( "encoding/json" "net/http" "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" ) // ============================================================================ // Hazard Library & Controls Library // ============================================================================ // 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), }) } // ============================================================================ // Hazard CRUD // ============================================================================ // 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", }) } // ============================================================================ // Risk Assessment // ============================================================================ // 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) // ISO 12100 mode: use ISO thresholds when avoidance is set var riskLevel iace.RiskLevel if req.Avoidance >= 1 { riskLevel = h.engine.DetermineRiskLevelISO(inherentRisk) } else { 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}) } // 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, }) }