Files
breakpilot-compliance/ai-compliance-sdk/internal/api/handlers/ucca_handlers.go
Sharang Parnerkar 9f96061631 refactor(go): split training/store, ucca/rules, ucca_handlers, document_export under 500 LOC
Each of the four oversized files (training/store.go 1569 LOC, ucca/rules.go 1231 LOC,
ucca_handlers.go 1135 LOC, document_export.go 1101 LOC) is split by logical group
into same-package files, all under the 500-line hard cap. Zero behavior changes,
no renamed exported symbols. Also fixed pre-existing hazard_library split (missing
functions and duplicate UUID keys from a prior session).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-19 09:29:54 +02:00

333 lines
11 KiB
Go

package handlers
import (
"crypto/sha256"
"encoding/hex"
"fmt"
"net/http"
"strconv"
"time"
"github.com/breakpilot/ai-compliance-sdk/internal/llm"
"github.com/breakpilot/ai-compliance-sdk/internal/rbac"
"github.com/breakpilot/ai-compliance-sdk/internal/ucca"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
)
// UCCAHandlers handles UCCA-related API endpoints
type UCCAHandlers struct {
store *ucca.Store
escalationStore *ucca.EscalationStore
policyEngine *ucca.PolicyEngine
legacyRuleEngine *ucca.RuleEngine // Keep for backwards compatibility
providerRegistry *llm.ProviderRegistry
legalRAGClient *ucca.LegalRAGClient
escalationTrigger *ucca.EscalationTrigger
}
// NewUCCAHandlers creates new UCCA handlers
func NewUCCAHandlers(store *ucca.Store, escalationStore *ucca.EscalationStore, providerRegistry *llm.ProviderRegistry) *UCCAHandlers {
// Try to create YAML-based policy engine first
policyEngine, err := ucca.NewPolicyEngine()
if err != nil {
// Log warning but don't fail - fall back to legacy engine
fmt.Printf("Warning: Could not load YAML policy engine: %v. Falling back to legacy rules.\n", err)
}
return &UCCAHandlers{
store: store,
escalationStore: escalationStore,
policyEngine: policyEngine, // May be nil if YAML loading failed
legacyRuleEngine: ucca.NewRuleEngine(),
providerRegistry: providerRegistry,
legalRAGClient: ucca.NewLegalRAGClient(),
escalationTrigger: ucca.DefaultEscalationTrigger(),
}
}
// evaluateIntake runs evaluation using YAML engine or legacy fallback
func (h *UCCAHandlers) evaluateIntake(intake *ucca.UseCaseIntake) (*ucca.AssessmentResult, string) {
if h.policyEngine != nil {
return h.policyEngine.Evaluate(intake), h.policyEngine.GetPolicyVersion()
}
return h.legacyRuleEngine.Evaluate(intake), "1.0.0-legacy"
}
// Assess evaluates a use case intake and creates an assessment
func (h *UCCAHandlers) Assess(c *gin.Context) {
tenantID := rbac.GetTenantID(c)
userID := rbac.GetUserID(c)
if tenantID == uuid.Nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "tenant ID required"})
return
}
var intake ucca.UseCaseIntake
if err := c.ShouldBindJSON(&intake); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
result, policyVersion := h.evaluateIntake(&intake)
// Calculate hash of use case text
hash := sha256.Sum256([]byte(intake.UseCaseText))
hashStr := hex.EncodeToString(hash[:])
assessment := &ucca.Assessment{
TenantID: tenantID,
Title: intake.Title,
PolicyVersion: policyVersion,
Status: "completed",
Intake: intake,
UseCaseTextStored: intake.StoreRawText,
UseCaseTextHash: hashStr,
Feasibility: result.Feasibility,
RiskLevel: result.RiskLevel,
Complexity: result.Complexity,
RiskScore: result.RiskScore,
TriggeredRules: result.TriggeredRules,
RequiredControls: result.RequiredControls,
RecommendedArchitecture: result.RecommendedArchitecture,
ForbiddenPatterns: result.ForbiddenPatterns,
ExampleMatches: result.ExampleMatches,
DSFARecommended: result.DSFARecommended,
Art22Risk: result.Art22Risk,
TrainingAllowed: result.TrainingAllowed,
Domain: intake.Domain,
CreatedBy: userID,
}
if !intake.StoreRawText {
assessment.Intake.UseCaseText = ""
}
if assessment.Title == "" {
assessment.Title = fmt.Sprintf("Assessment vom %s", time.Now().Format("02.01.2006 15:04"))
}
if err := h.store.CreateAssessment(c.Request.Context(), assessment); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
escalation := h.createEscalationForAssessment(c, assessment, result, tenantID, userID)
c.JSON(http.StatusCreated, ucca.AssessResponse{
Assessment: *assessment,
Result: *result,
Escalation: escalation,
})
}
// ListAssessments returns all assessments for a tenant
func (h *UCCAHandlers) ListAssessments(c *gin.Context) {
tenantID := rbac.GetTenantID(c)
if tenantID == uuid.Nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "tenant ID required"})
return
}
filters := &ucca.AssessmentFilters{
Feasibility: c.Query("feasibility"),
Domain: c.Query("domain"),
RiskLevel: c.Query("risk_level"),
Search: c.Query("search"),
}
if limit, err := strconv.Atoi(c.DefaultQuery("limit", "0")); err == nil {
filters.Limit = limit
}
if offset, err := strconv.Atoi(c.DefaultQuery("offset", "0")); err == nil {
filters.Offset = offset
}
assessments, total, err := h.store.ListAssessments(c.Request.Context(), tenantID, filters)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"assessments": assessments, "total": total})
}
// GetAssessment returns a single assessment by ID
func (h *UCCAHandlers) GetAssessment(c *gin.Context) {
id, err := uuid.Parse(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid ID"})
return
}
assessment, err := h.store.GetAssessment(c.Request.Context(), id)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
if assessment == nil {
c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
return
}
c.JSON(http.StatusOK, assessment)
}
// DeleteAssessment deletes an assessment
func (h *UCCAHandlers) DeleteAssessment(c *gin.Context) {
id, err := uuid.Parse(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid ID"})
return
}
if err := h.store.DeleteAssessment(c.Request.Context(), id); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "deleted"})
}
// UpdateAssessment re-evaluates and updates an existing assessment
func (h *UCCAHandlers) UpdateAssessment(c *gin.Context) {
id, err := uuid.Parse(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid ID"})
return
}
var intake ucca.UseCaseIntake
if err := c.ShouldBindJSON(&intake); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
result, policyVersion := h.evaluateIntake(&intake)
hash := sha256.Sum256([]byte(intake.UseCaseText))
hashStr := hex.EncodeToString(hash[:])
updated := &ucca.Assessment{
Title: intake.Title,
PolicyVersion: policyVersion,
Intake: intake,
UseCaseTextStored: intake.StoreRawText,
UseCaseTextHash: hashStr,
Feasibility: result.Feasibility,
RiskLevel: result.RiskLevel,
Complexity: result.Complexity,
RiskScore: result.RiskScore,
TriggeredRules: result.TriggeredRules,
RequiredControls: result.RequiredControls,
RecommendedArchitecture: result.RecommendedArchitecture,
ForbiddenPatterns: result.ForbiddenPatterns,
ExampleMatches: result.ExampleMatches,
DSFARecommended: result.DSFARecommended,
Art22Risk: result.Art22Risk,
TrainingAllowed: result.TrainingAllowed,
Domain: intake.Domain,
}
if !intake.StoreRawText {
updated.Intake.UseCaseText = ""
}
if updated.Title == "" {
updated.Title = fmt.Sprintf("Assessment vom %s", time.Now().Format("02.01.2006 15:04"))
}
if err := h.store.UpdateAssessment(c.Request.Context(), id, updated); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
assessment, err := h.store.GetAssessment(c.Request.Context(), id)
if err != nil || assessment == nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "assessment not found after update"})
return
}
c.JSON(http.StatusOK, assessment)
}
// GetStats returns UCCA statistics for a tenant
func (h *UCCAHandlers) GetStats(c *gin.Context) {
tenantID := rbac.GetTenantID(c)
if tenantID == uuid.Nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "tenant ID required"})
return
}
stats, err := h.store.GetStats(c.Request.Context(), tenantID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, stats)
}
// createEscalationForAssessment automatically creates an escalation based on assessment result
func (h *UCCAHandlers) createEscalationForAssessment(c *gin.Context, assessment *ucca.Assessment, result *ucca.AssessmentResult, tenantID, userID uuid.UUID) *ucca.Escalation {
if h.escalationStore == nil || h.escalationTrigger == nil {
return nil
}
level, reason := h.escalationTrigger.DetermineEscalationLevel(result)
responseHours, _ := ucca.GetDefaultSLA(level)
var dueDate *time.Time
if responseHours > 0 {
due := time.Now().UTC().Add(time.Duration(responseHours) * time.Hour)
dueDate = &due
}
escalation := &ucca.Escalation{
TenantID: tenantID,
AssessmentID: assessment.ID,
EscalationLevel: level,
EscalationReason: reason,
Status: ucca.EscalationStatusPending,
DueDate: dueDate,
}
if level == ucca.EscalationLevelE0 {
escalation.Status = ucca.EscalationStatusApproved
approveDecision := ucca.EscalationDecisionApprove
escalation.Decision = &approveDecision
now := time.Now().UTC()
escalation.DecisionAt = &now
autoNotes := "Automatische Freigabe (E0)"
escalation.DecisionNotes = &autoNotes
}
if err := h.escalationStore.CreateEscalation(c.Request.Context(), escalation); err != nil {
fmt.Printf("Warning: Could not create escalation: %v\n", err)
return nil
}
h.escalationStore.AddEscalationHistory(c.Request.Context(), &ucca.EscalationHistory{
EscalationID: escalation.ID,
Action: "auto_created",
NewStatus: string(escalation.Status),
NewLevel: string(escalation.EscalationLevel),
ActorID: userID,
Notes: "Automatisch erstellt bei Assessment",
})
if level != ucca.EscalationLevelE0 {
role := ucca.GetRoleForLevel(level)
reviewer, err := h.escalationStore.GetNextAvailableReviewer(c.Request.Context(), tenantID, role)
if err == nil && reviewer != nil {
h.escalationStore.AssignEscalation(c.Request.Context(), escalation.ID, reviewer.UserID, role)
h.escalationStore.IncrementReviewerCount(c.Request.Context(), reviewer.UserID)
h.escalationStore.AddEscalationHistory(c.Request.Context(), &ucca.EscalationHistory{
EscalationID: escalation.ID,
Action: "auto_assigned",
OldStatus: string(ucca.EscalationStatusPending),
NewStatus: string(ucca.EscalationStatusAssigned),
ActorID: userID,
Notes: "Automatisch zugewiesen an: " + reviewer.UserName,
})
}
}
return escalation
}