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>
333 lines
11 KiB
Go
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
|
|
}
|