refactor(go/handlers): split iace_handler and training_handlers into focused files
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>
This commit is contained in:
469
ai-compliance-sdk/internal/api/handlers/iace_handler_hazards.go
Normal file
469
ai-compliance-sdk/internal/api/handlers/iace_handler_hazards.go
Normal file
@@ -0,0 +1,469 @@
|
||||
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,
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user