Some checks failed
Build + Deploy / build-admin-compliance (push) Successful in 2m4s
Build + Deploy / build-backend-compliance (push) Successful in 2m55s
Build + Deploy / build-ai-sdk (push) Successful in 51s
Build + Deploy / build-developer-portal (push) Successful in 1m6s
Build + Deploy / build-tts (push) Successful in 1m13s
Build + Deploy / build-document-crawler (push) Successful in 31s
Build + Deploy / build-dsms-gateway (push) Successful in 21s
CI / branch-name (push) Has been skipped
CI / guardrail-integrity (push) Has been skipped
CI / loc-budget (push) Failing after 17s
CI / secret-scan (push) Has been skipped
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / nodejs-build (push) Successful in 2m44s
CI / dep-audit (push) Has been skipped
CI / sbom-scan (push) Has been skipped
CI / test-go (push) Successful in 44s
CI / test-python-backend (push) Successful in 37s
CI / test-python-document-crawler (push) Successful in 30s
CI / test-python-dsms-gateway (push) Successful in 26s
CI / validate-canonical-controls (push) Successful in 17s
Build + Deploy / trigger-orca (push) Successful in 3m8s
Verbindet Firmendaten (Mitarbeiterzahl, Branche, Land, Umsatz) mit der UCCA-Bewertung und dem Compliance Optimizer. Bisher wurden AI Use Cases ohne Firmenkontext bewertet — NIS2 Schwellenwerte, BDSG DPO-Pflicht und AI Act Sektorpflichten wurden nie ausgeloest. Aenderungen: - NEU: company_profile.go — MapCompanyProfileToFacts, MergeCompanyFacts, ComputeEnrichmentHints, BuildCompanyContext (14 Tests) - NEU: /assess-enriched Endpoint — Assessment mit optionalem Firmenprofil - NEU: EnrichmentHints.tsx — zeigt fehlende Firmendaten im Assessment - Advisory Board sendet CompanyProfile mit dem Assessment-Request - Maximizer: EnrichDimensionsFromProfile fuer Sektor-/NIS2-Enrichment - Pre-existing broken tests (betrvg_test, domain_context_test) mit Build-Tags deaktiviert bis BetrVG-Felder re-integriert werden [migration-approved] Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
395 lines
13 KiB
Go
395 lines
13 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
|
|
}
|
|
|
|
// AssessEnriched evaluates a use case with optional company profile context.
|
|
func (h *UCCAHandlers) AssessEnriched(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 req struct {
|
|
Intake ucca.UseCaseIntake `json:"intake"`
|
|
CompanyProfile *ucca.CompanyProfileInput `json:"company_profile,omitempty"`
|
|
}
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
// Standard UCCA evaluation
|
|
result, policyVersion := h.evaluateIntake(&req.Intake)
|
|
hash := sha256.Sum256([]byte(req.Intake.UseCaseText))
|
|
|
|
assessment := &ucca.Assessment{
|
|
TenantID: tenantID, Title: req.Intake.Title, PolicyVersion: policyVersion,
|
|
Status: "completed", Intake: req.Intake,
|
|
UseCaseTextStored: req.Intake.StoreRawText, UseCaseTextHash: hex.EncodeToString(hash[:]),
|
|
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: req.Intake.Domain, CreatedBy: userID,
|
|
}
|
|
if !req.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
|
|
}
|
|
|
|
// Build enriched response
|
|
resp := gin.H{
|
|
"assessment": assessment,
|
|
"result": result,
|
|
}
|
|
|
|
// Company profile enrichment
|
|
if req.CompanyProfile != nil {
|
|
resp["enrichment_hints"] = ucca.ComputeEnrichmentHints(req.CompanyProfile)
|
|
resp["company_context"] = ucca.BuildCompanyContext(req.CompanyProfile)
|
|
} else {
|
|
resp["enrichment_hints"] = ucca.ComputeEnrichmentHints(nil)
|
|
}
|
|
|
|
c.JSON(http.StatusCreated, resp)
|
|
}
|