Files
breakpilot-compliance/ai-compliance-sdk/internal/api/handlers/gap_handler.go
T
Benjamin Admin 66d30568e2
Build + Deploy / build-admin-compliance (push) Successful in 1m41s
Build + Deploy / build-backend-compliance (push) Successful in 14s
Build + Deploy / build-ai-sdk (push) Successful in 41s
Build + Deploy / build-developer-portal (push) Successful in 10s
Build + Deploy / build-tts (push) Successful in 10s
Build + Deploy / build-document-crawler (push) Successful in 10s
Build + Deploy / build-dsms-gateway (push) Successful in 10s
Build + Deploy / build-dsms-node (push) Successful in 11s
CI / branch-name (push) Has been skipped
CI / guardrail-integrity (push) Has been skipped
CI / loc-budget (push) Failing after 14s
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 2m31s
CI / dep-audit (push) Has been skipped
CI / sbom-scan (push) Has been skipped
CI / test-go (push) Successful in 48s
CI / test-python-backend (push) Failing after 1s
CI / test-python-document-crawler (push) Successful in 32s
CI / test-python-dsms-gateway (push) Successful in 25s
CI / validate-canonical-controls (push) Successful in 15s
Build + Deploy / trigger-orca (push) Successful in 2m23s
feat(dsms): Stufe 1 — Gap-Analyse Report wird in DSMS archiviert
- Go DSMS Client (internal/dsms/client.go): Archive() + Verify()
- Python DSMS Client (compliance/services/dsms_client.py): archive_to_dsms() + verify_dsms()
- Gap-Analyse AnalyzeProject() archiviert Report-JSON nach DSMS
- Response enthält dsms_cid wenn Archivierung erfolgreich
- Frontend: Grünes "Revisionssicher archiviert" Badge mit CID im GapDashboard
- DSMS Proxy Route (/api/sdk/v1/dsms/[...path]) für Verify-Abfragen

Stufe 2 (Evidence Upload → DSMS) und Stufe 3 (Version Chains) folgen.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-11 23:39:26 +02:00

193 lines
5.1 KiB
Go

package handlers
import (
"encoding/json"
"fmt"
"net/http"
"time"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"github.com/jackc/pgx/v5/pgxpool"
"github.com/breakpilot/ai-compliance-sdk/internal/dsms"
"github.com/breakpilot/ai-compliance-sdk/internal/gap"
)
// GapHandler handles regulatory gap analysis endpoints.
type GapHandler struct {
engine *gap.Engine
store *gap.Store
}
// NewGapHandler creates a new GapHandler.
func NewGapHandler(pool *pgxpool.Pool) *GapHandler {
store := gap.NewStore(pool)
return &GapHandler{
engine: gap.NewEngine(store),
store: store,
}
}
// CreateProject creates a new gap analysis project.
// POST /sdk/v1/gap/projects
func (h *GapHandler) CreateProject(c *gin.Context) {
var profile gap.ProductProfile
if err := c.ShouldBindJSON(&profile); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
tenantID := c.GetHeader("X-Tenant-ID")
if tenantID != "" {
profile.TenantID, _ = uuid.Parse(tenantID)
}
if err := h.store.CreateProfile(&profile); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to create project"})
return
}
c.JSON(http.StatusCreated, gin.H{"project": profile})
}
// GetProject returns a gap project by ID.
// GET /sdk/v1/gap/projects/:id
func (h *GapHandler) GetProject(c *gin.Context) {
id, err := uuid.Parse(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project ID"})
return
}
profile, err := h.store.GetProfile(id)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "project not found"})
return
}
c.JSON(http.StatusOK, gin.H{"project": profile})
}
// ListProjects lists gap projects for a tenant.
// GET /sdk/v1/gap/projects
func (h *GapHandler) ListProjects(c *gin.Context) {
tenantID, err := uuid.Parse(c.GetHeader("X-Tenant-ID"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "X-Tenant-ID required"})
return
}
profiles, err := h.store.ListProfiles(tenantID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to list projects"})
return
}
c.JSON(http.StatusOK, gin.H{"projects": profiles, "total": len(profiles)})
}
// AnalyzeProject runs the full gap analysis.
// POST /sdk/v1/gap/projects/:id/analyze
func (h *GapHandler) AnalyzeProject(c *gin.Context) {
id, err := uuid.Parse(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project ID"})
return
}
profile, err := h.store.GetProfile(id)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "project not found"})
return
}
report, err := h.engine.Analyze(profile)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
// Archive gap report to DSMS (non-blocking, best-effort)
var dsmsCID string
reportJSON, _ := json.Marshal(report)
filename := fmt.Sprintf("gap-report-%s-%s.json", id.String()[:8], time.Now().Format("2006-01-02"))
if result := dsms.Archive(reportJSON, filename, "gap_report", id.String(), "1"); result != nil {
dsmsCID = result.CID
}
// Return report with DSMS CID appended
response := gin.H{
"profile_id": report.ProfileID,
"profile_name": report.ProfileName,
"regulations": report.Regulations,
"summary": report.Summary,
"gaps": report.Gaps,
"created_at": report.CreatedAt,
}
if dsmsCID != "" {
response["dsms_cid"] = dsmsCID
}
c.JSON(http.StatusOK, response)
}
// QuickAnalyze runs gap analysis without saving a project.
// POST /sdk/v1/gap/analyze
func (h *GapHandler) QuickAnalyze(c *gin.Context) {
var profile gap.ProductProfile
if err := c.ShouldBindJSON(&profile); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
profile.ID = uuid.New()
report, err := h.engine.Analyze(&profile)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, report)
}
// GetTemplates returns available industry templates.
// GET /sdk/v1/gap/templates
func (h *GapHandler) GetTemplates(c *gin.Context) {
templates := make([]gin.H, 0, len(gap.IndustryTemplates))
for key, tmpl := range gap.IndustryTemplates {
templates = append(templates, gin.H{
"key": key,
"name": tmpl.Name,
"description": tmpl.Description,
"product_type": tmpl.ProductType,
})
}
c.JSON(http.StatusOK, gin.H{"templates": templates})
}
// GetTemplate returns a specific template by key.
// GET /sdk/v1/gap/templates/:key
func (h *GapHandler) GetTemplate(c *gin.Context) {
key := c.Param("key")
tmpl, ok := gap.IndustryTemplates[key]
if !ok {
c.JSON(http.StatusNotFound, gin.H{"error": "template not found"})
return
}
c.JSON(http.StatusOK, gin.H{"template": tmpl})
}
// GetRegulations returns all supported regulations with deadlines.
// GET /sdk/v1/gap/regulations
func (h *GapHandler) GetRegulations(c *gin.Context) {
regs := make([]gin.H, 0, len(gap.RegulationNames))
for id, name := range gap.RegulationNames {
entry := gin.H{"id": id, "name": name}
if dl, ok := gap.RegulationDeadlines[id]; ok {
entry["deadline"] = dl
}
regs = append(regs, entry)
}
c.JSON(http.StatusOK, gin.H{"regulations": regs})
}