dabc2358ab
Product Profile → Regulatory Classification → MC Gap Assessment → Priority List. - 12 regulations supported (CRA, AI Act, NIS2, DSGVO, Data Act, MiCA, PSD2, AML, MDR, Machinery, TDDDG, LkSG) - Scope signal extraction from product profile - Priority scoring: Severity × Deadline × Dependency - 5 industry templates (IoT, Exchange, Cobot, SaaS, Medical) - 8 API endpoints under /sdk/v1/gap/ - DB migration for gap_projects table - Full build passes Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
169 lines
4.4 KiB
Go
169 lines
4.4 KiB
Go
package handlers
|
|
|
|
import (
|
|
"net/http"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/google/uuid"
|
|
"github.com/jackc/pgx/v5/pgxpool"
|
|
|
|
"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
|
|
}
|
|
|
|
c.JSON(http.StatusOK, report)
|
|
}
|
|
|
|
// 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})
|
|
}
|