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>
470 lines
14 KiB
Go
470 lines
14 KiB
Go
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,
|
|
})
|
|
}
|