Compare commits

...

2 Commits

Author SHA1 Message Date
Benjamin Admin 51d91d20ed fix: 6 false positives from Stadt Koeln + Caritas verification
Build + Deploy / build-admin-compliance (push) Successful in 9s
Build + Deploy / build-backend-compliance (push) Successful in 8s
Build + Deploy / build-ai-sdk (push) Successful in 40s
Build + Deploy / build-developer-portal (push) Successful in 7s
Build + Deploy / build-tts (push) Successful in 8s
Build + Deploy / build-document-crawler (push) Successful in 8s
Build + Deploy / build-dsms-gateway (push) Successful in 8s
Build + Deploy / build-dsms-node (push) Successful in 8s
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 3m11s
CI / dep-audit (push) Has been skipped
CI / sbom-scan (push) Has been skipped
CI / test-go (push) Failing after 45s
CI / test-python-backend (push) Successful in 41s
CI / test-python-document-crawler (push) Successful in 29s
CI / test-python-dsms-gateway (push) Successful in 27s
CI / validate-canonical-controls (push) Successful in 17s
Build + Deploy / trigger-orca (push) Successful in 2m23s
- Phone regex allows parentheses: +49 (0)761 now matches
- "Recht auf Widerspruch" (3 words) + §23 KDG recognized
- Church authorities: "Katholisches Datenschutzzentrum", KdoeR
- "Artikel 6 Absatz 1 Buchstabe a" (unabbreviated) now matches
- "PHP Session ID" (with spaces) alongside "PHPSESSID"

6 FP eliminated across Caritas (KDG) and Stadt Koeln (verbose forms).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-08 01:31:36 +02:00
Benjamin Admin 8087e74e88 feat: Verification handler split + ListVerificationPlans
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-08 01:19:13 +02:00
3 changed files with 329 additions and 1 deletions
@@ -0,0 +1,327 @@
package handlers
import (
"encoding/json"
"net/http"
"github.com/breakpilot/ai-compliance-sdk/internal/iace"
"github.com/breakpilot/ai-compliance-sdk/internal/rbac"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
)
// ============================================================================
// Evidence & Verification Plans
// ============================================================================
// UploadEvidence handles POST /projects/:id/evidence
// Creates a new evidence record for a project.
func (h *IACEHandler) UploadEvidence(c *gin.Context) {
projectID, err := uuid.Parse(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project ID"})
return
}
var req struct {
MitigationID *uuid.UUID `json:"mitigation_id,omitempty"`
VerificationPlanID *uuid.UUID `json:"verification_plan_id,omitempty"`
FileName string `json:"file_name" binding:"required"`
FilePath string `json:"file_path" binding:"required"`
FileHash string `json:"file_hash" binding:"required"`
FileSize int64 `json:"file_size" binding:"required"`
MimeType string `json:"mime_type" binding:"required"`
Description string `json:"description,omitempty"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
userID := rbac.GetUserID(c)
evidence := &iace.Evidence{
ProjectID: projectID,
MitigationID: req.MitigationID,
VerificationPlanID: req.VerificationPlanID,
FileName: req.FileName,
FilePath: req.FilePath,
FileHash: req.FileHash,
FileSize: req.FileSize,
MimeType: req.MimeType,
Description: req.Description,
UploadedBy: userID,
}
if err := h.store.CreateEvidence(c.Request.Context(), evidence); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
newVals, _ := json.Marshal(evidence)
h.store.AddAuditEntry(
c.Request.Context(), projectID, "evidence", evidence.ID,
iace.AuditActionCreate, userID.String(), nil, newVals,
)
c.JSON(http.StatusCreated, gin.H{"evidence": evidence})
}
// ListEvidence handles GET /projects/:id/evidence
func (h *IACEHandler) ListEvidence(c *gin.Context) {
projectID, err := uuid.Parse(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project ID"})
return
}
evidence, err := h.store.ListEvidence(c.Request.Context(), projectID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
if evidence == nil {
evidence = []iace.Evidence{}
}
c.JSON(http.StatusOK, gin.H{"evidence": evidence, "total": len(evidence)})
}
// CreateVerificationPlan handles POST /projects/:id/verification-plan
func (h *IACEHandler) CreateVerificationPlan(c *gin.Context) {
projectID, err := uuid.Parse(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project ID"})
return
}
var req iace.CreateVerificationPlanRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
req.ProjectID = projectID
plan, err := h.store.CreateVerificationPlan(c.Request.Context(), req)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
userID := rbac.GetUserID(c)
newVals, _ := json.Marshal(plan)
h.store.AddAuditEntry(
c.Request.Context(), projectID, "verification_plan", plan.ID,
iace.AuditActionCreate, userID.String(), nil, newVals,
)
c.JSON(http.StatusCreated, gin.H{"verification_plan": plan})
}
// UpdateVerificationPlan handles PUT /verification-plan/:vid
func (h *IACEHandler) UpdateVerificationPlan(c *gin.Context) {
planID, err := uuid.Parse(c.Param("vid"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid verification plan ID"})
return
}
var updates map[string]interface{}
if err := c.ShouldBindJSON(&updates); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
plan, err := h.store.UpdateVerificationPlan(c.Request.Context(), planID, updates)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
if plan == nil {
c.JSON(http.StatusNotFound, gin.H{"error": "verification plan not found"})
return
}
c.JSON(http.StatusOK, gin.H{"verification_plan": plan})
}
// CompleteVerification handles POST /verification-plan/:vid/complete
func (h *IACEHandler) CompleteVerification(c *gin.Context) {
planID, err := uuid.Parse(c.Param("vid"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid verification plan ID"})
return
}
var req struct {
Result string `json:"result" binding:"required"`
Passed *bool `json:"passed"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
userID := rbac.GetUserID(c)
if err := h.store.CompleteVerification(
c.Request.Context(), planID, req.Result, userID.String(),
); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "verification completed"})
}
// ============================================================================
// Frontend-compatible /verifications routes
// ============================================================================
// ListVerificationPlans handles GET /projects/:id/verifications
func (h *IACEHandler) ListVerificationPlans(c *gin.Context) {
projectID, err := uuid.Parse(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project ID"})
return
}
plans, err := h.store.ListVerificationPlans(c.Request.Context(), projectID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
// Enrich with linked hazard/mitigation names
hazards, _ := h.store.ListHazards(c.Request.Context(), projectID)
mitigations, _ := h.store.ListMitigationsByProject(c.Request.Context(), projectID)
hazardMap := make(map[uuid.UUID]string, len(hazards))
for _, hz := range hazards {
hazardMap[hz.ID] = hz.Name
}
mitMap := make(map[uuid.UUID]string, len(mitigations))
for _, m := range mitigations {
mitMap[m.ID] = m.Name
}
type enrichedPlan struct {
iace.VerificationPlan
LinkedHazardName string `json:"linked_hazard_name,omitempty"`
LinkedMitigationName string `json:"linked_mitigation_name,omitempty"`
}
enriched := make([]enrichedPlan, 0, len(plans))
for _, p := range plans {
ep := enrichedPlan{VerificationPlan: p}
if p.HazardID != nil {
ep.LinkedHazardName = hazardMap[*p.HazardID]
}
if p.MitigationID != nil {
ep.LinkedMitigationName = mitMap[*p.MitigationID]
}
enriched = append(enriched, ep)
}
c.JSON(http.StatusOK, gin.H{"verifications": enriched, "total": len(enriched)})
}
// CreateVerificationAlias handles POST /projects/:id/verifications
// Frontend-compatible alias that maps linked_mitigation_id to mitigation_id.
func (h *IACEHandler) CreateVerificationAlias(c *gin.Context) {
projectID, err := uuid.Parse(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project ID"})
return
}
var body struct {
Title string `json:"title" binding:"required"`
Description string `json:"description"`
Method string `json:"method" binding:"required"`
LinkedMitigationID *string `json:"linked_mitigation_id"`
LinkedHazardID *string `json:"linked_hazard_id"`
MitigationID *string `json:"mitigation_id"`
HazardID *string `json:"hazard_id"`
}
if err := c.ShouldBindJSON(&body); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
req := iace.CreateVerificationPlanRequest{
ProjectID: projectID,
Title: body.Title,
Description: body.Description,
Method: iace.VerificationMethod(body.Method),
}
midStr := body.MitigationID
if midStr == nil {
midStr = body.LinkedMitigationID
}
if midStr != nil {
mid, err := uuid.Parse(*midStr)
if err == nil {
req.MitigationID = &mid
}
}
hidStr := body.HazardID
if hidStr == nil {
hidStr = body.LinkedHazardID
}
if hidStr != nil {
hid, err := uuid.Parse(*hidStr)
if err == nil {
req.HazardID = &hid
}
}
plan, err := h.store.CreateVerificationPlan(c.Request.Context(), req)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
userID := rbac.GetUserID(c)
newVals, _ := json.Marshal(plan)
h.store.AddAuditEntry(
c.Request.Context(), projectID, "verification_plan", plan.ID,
iace.AuditActionCreate, userID.String(), nil, newVals,
)
c.JSON(http.StatusCreated, plan)
}
// DeleteVerificationPlan handles DELETE /projects/:id/verifications/:vid
func (h *IACEHandler) DeleteVerificationPlan(c *gin.Context) {
projectID, err := uuid.Parse(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project ID"})
return
}
planID, err := uuid.Parse(c.Param("vid"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid verification plan ID"})
return
}
if err := h.store.DeleteVerificationPlan(c.Request.Context(), planID); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
userID := rbac.GetUserID(c)
h.store.AddAuditEntry(
c.Request.Context(), projectID, "verification_plan", planID,
iace.AuditActionDelete, userID.String(), nil, nil,
)
c.JSON(http.StatusOK, gin.H{"message": "verification plan deleted"})
}
// CompleteVerificationAlias handles POST /projects/:id/verifications/:vid/complete
func (h *IACEHandler) CompleteVerificationAlias(c *gin.Context) {
h.CompleteVerification(c)
}
@@ -23,7 +23,7 @@ COOKIE_CHECKLIST = [
"label": "Konkrete Cookie-Namen aufgelistet",
"level": 2, "parent": "cookie_types",
"patterns": [
r"(?:_ga|_gid|_gat|_fbp|_gcl|phpsessid|jsessionid|csrf|xsrf|cookieinfo|et_id|bt_\w+|cntcookie|shophk)",
r"(?:_ga|_gid|_gat|_fbp|_gcl|phpsessid|php\s+session\s+id|jsessionid|csrf|xsrf|cookieinfo|et_id|bt_\w+|cntcookie|shophk)",
r"cookie[\-_]?name\s*[:\|]",
r"name\s+des\s+cookie",
r"(?:name|bezeichnung)\s+.*(?:funktion|zweck|speicherdauer|laufzeit)",
@@ -213,6 +213,7 @@ JOINT_CONTROLLER_CHECKLIST = [
"level": 2, "parent": "legal_basis",
"patterns": [
r"art\.\s*6\s*(?:abs\.\s*)?1\s*(?:s\.\s*1\s*)?(?:lit\.\s*)?[a-f]",
r"artikel\s*6\s*(?:absatz\s*)?1\s*(?:buchstabe\s*)?[a-f]",
],
"severity": "LOW",
"hint": "Praxistipp: Nennen Sie pro Verarbeitungszweck den passenden Buchstaben. Typisch bei Social Media: Art. 6(1) lit. a (Einwilligung bei Direktnachrichten), lit. b (Vertrag bei Gewinnspielen), lit. f (berechtigtes Interesse bei Insights/PR).",