feat(iace): benchmark system + erklaerteil + dedup-fix
Build + Deploy / build-backend-compliance (push) Successful in 3m34s
Build + Deploy / build-ai-sdk (push) Successful in 1m6s
Build + Deploy / build-developer-portal (push) Successful in 1m7s
Build + Deploy / build-tts (push) Successful in 1m58s
Build + Deploy / build-document-crawler (push) Successful in 57s
Build + Deploy / build-dsms-gateway (push) Successful in 34s
Build + Deploy / build-admin-compliance (push) Successful in 2m7s
Build + Deploy / build-dsms-node (push) Successful in 29s
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 2m28s
CI / dep-audit (push) Has been skipped
CI / sbom-scan (push) Has been skipped
CI / test-go (push) Successful in 42s
CI / test-python-backend (push) Successful in 37s
CI / test-python-document-crawler (push) Successful in 27s
CI / test-python-dsms-gateway (push) Successful in 22s
CI / validate-canonical-controls (push) Successful in 15s
Build + Deploy / trigger-orca (push) Successful in 3m10s
Build + Deploy / build-backend-compliance (push) Successful in 3m34s
Build + Deploy / build-ai-sdk (push) Successful in 1m6s
Build + Deploy / build-developer-portal (push) Successful in 1m7s
Build + Deploy / build-tts (push) Successful in 1m58s
Build + Deploy / build-document-crawler (push) Successful in 57s
Build + Deploy / build-dsms-gateway (push) Successful in 34s
Build + Deploy / build-admin-compliance (push) Successful in 2m7s
Build + Deploy / build-dsms-node (push) Successful in 29s
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 2m28s
CI / dep-audit (push) Has been skipped
CI / sbom-scan (push) Has been skipped
CI / test-go (push) Successful in 42s
CI / test-python-backend (push) Successful in 37s
CI / test-python-document-crawler (push) Successful in 27s
CI / test-python-dsms-gateway (push) Successful in 22s
CI / validate-canonical-controls (push) Successful in 15s
Build + Deploy / trigger-orca (push) Successful in 3m10s
- Erklaerteil-Template fuer Risikobeurteilungen (risk_assessment_template.go) in PDF-Export, Markdown-Export und Frontend ReportPrintView eingebaut - Ground Truth Benchmark-System: Datenmodell, Fuzzy-Matching-Engine, 3 API Endpoints (import-gt, benchmark, benchmark/summary) - Frontend Benchmark-Tab mit Score-Cards, Kategorie-Breakdown, Hazard-Vergleichstabelle (Zugeordnet/Fehlend/Extra), Business Impact - Erster Benchmark: 13.3% Coverage (Baseline) gegen 60 GT-Eintraege - Dedup-Fix: seenCat[cat] -> seenCatZone[cat+zone] erlaubt mehrere Gefaehrdungen pro Kategorie an verschiedenen Gefahrenstellen - Komponenten-spezifische Hazard-Namen und Zone-basierte Zuordnung Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,162 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/breakpilot/ai-compliance-sdk/internal/iace"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// ImportGroundTruth handles POST /projects/:id/benchmark/import-gt
|
||||
// Stores Ground Truth data in project metadata.ground_truth.
|
||||
func (h *IACEHandler) ImportGroundTruth(c *gin.Context) {
|
||||
projectID, err := uuid.Parse(c.Param("id"))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project ID"})
|
||||
return
|
||||
}
|
||||
|
||||
ctx := c.Request.Context()
|
||||
project, err := h.store.GetProject(ctx, projectID)
|
||||
if err != nil || project == nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "project not found"})
|
||||
return
|
||||
}
|
||||
|
||||
var gt iace.GroundTruth
|
||||
if err := c.ShouldBindJSON(>); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid ground truth JSON: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
if gt.ImportedAt == "" {
|
||||
gt.ImportedAt = time.Now().Format("2006-01-02")
|
||||
}
|
||||
|
||||
// Merge into existing metadata
|
||||
meta := make(map[string]json.RawMessage)
|
||||
if project.Metadata != nil {
|
||||
_ = json.Unmarshal(project.Metadata, &meta)
|
||||
}
|
||||
gtJSON, _ := json.Marshal(gt)
|
||||
meta["ground_truth"] = gtJSON
|
||||
|
||||
mergedMeta, _ := json.Marshal(meta)
|
||||
err = h.store.UpdateProjectMetadata(ctx, projectID, mergedMeta)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to store ground truth"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"message": "ground truth imported",
|
||||
"entry_count": len(gt.Entries),
|
||||
"source_file": gt.SourceFile,
|
||||
})
|
||||
}
|
||||
|
||||
// RunBenchmark handles GET /projects/:id/benchmark?gt_project_id=:gtId
|
||||
// Compares engine hazards from project :id against GT from project :gtId.
|
||||
// If gt_project_id is omitted, looks for GT in the same project's metadata.
|
||||
func (h *IACEHandler) RunBenchmark(c *gin.Context) {
|
||||
projectID, err := uuid.Parse(c.Param("id"))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project ID"})
|
||||
return
|
||||
}
|
||||
|
||||
ctx := c.Request.Context()
|
||||
|
||||
// Determine GT source
|
||||
gtProjectID := projectID
|
||||
if gtParam := c.Query("gt_project_id"); gtParam != "" {
|
||||
parsed, err := uuid.Parse(gtParam)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid gt_project_id"})
|
||||
return
|
||||
}
|
||||
gtProjectID = parsed
|
||||
}
|
||||
|
||||
// Load GT
|
||||
gtProject, err := h.store.GetProject(ctx, gtProjectID)
|
||||
if err != nil || gtProject == nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "GT project not found"})
|
||||
return
|
||||
}
|
||||
gt, err := iace.ParseGroundTruth(gtProject.Metadata)
|
||||
if err != nil || gt == nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "no ground truth data in project metadata"})
|
||||
return
|
||||
}
|
||||
|
||||
// Load engine hazards + mitigations
|
||||
hazards, err := h.store.ListHazards(ctx, projectID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to load hazards"})
|
||||
return
|
||||
}
|
||||
mitigations, err := h.store.ListMitigationsByProject(ctx, projectID)
|
||||
if err != nil {
|
||||
mitigations = nil
|
||||
}
|
||||
|
||||
result := iace.CompareBenchmark(gt, hazards, mitigations)
|
||||
c.JSON(http.StatusOK, result)
|
||||
}
|
||||
|
||||
// GetBenchmarkSummary handles GET /projects/:id/benchmark/summary
|
||||
// Returns lightweight coverage metrics without full match details.
|
||||
func (h *IACEHandler) GetBenchmarkSummary(c *gin.Context) {
|
||||
projectID, err := uuid.Parse(c.Param("id"))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project ID"})
|
||||
return
|
||||
}
|
||||
|
||||
ctx := c.Request.Context()
|
||||
|
||||
gtProjectID := projectID
|
||||
if gtParam := c.Query("gt_project_id"); gtParam != "" {
|
||||
parsed, err := uuid.Parse(gtParam)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid gt_project_id"})
|
||||
return
|
||||
}
|
||||
gtProjectID = parsed
|
||||
}
|
||||
|
||||
gtProject, err := h.store.GetProject(ctx, gtProjectID)
|
||||
if err != nil || gtProject == nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "GT project not found"})
|
||||
return
|
||||
}
|
||||
gt, err := iace.ParseGroundTruth(gtProject.Metadata)
|
||||
if err != nil || gt == nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "no ground truth data"})
|
||||
return
|
||||
}
|
||||
|
||||
hazards, err := h.store.ListHazards(ctx, projectID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to load hazards"})
|
||||
return
|
||||
}
|
||||
mitigations, _ := h.store.ListMitigationsByProject(ctx, projectID)
|
||||
|
||||
result := iace.CompareBenchmark(gt, hazards, mitigations)
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"coverage_score": result.CoverageScore,
|
||||
"measure_coverage": result.MeasureCoverage,
|
||||
"total_gt": result.TotalGT,
|
||||
"total_engine": result.TotalEngine,
|
||||
"matched_count": len(result.MatchedPairs),
|
||||
"missing_count": len(result.MissingFromEngine),
|
||||
"extra_count": len(result.ExtraInEngine),
|
||||
"category_breakdown": result.CategoryBreakdown,
|
||||
})
|
||||
}
|
||||
@@ -143,26 +143,53 @@ func (h *IACEHandler) InitializeProject(c *gin.Context) {
|
||||
if len(existingHazards) == 0 && len(matchOutput.MatchedPatterns) > 0 {
|
||||
comps, _ := h.store.ListComponents(ctx, projectID)
|
||||
var defaultCompID uuid.UUID
|
||||
compByName := make(map[string]uuid.UUID)
|
||||
if len(comps) > 0 {
|
||||
defaultCompID = comps[0].ID
|
||||
for _, c := range comps {
|
||||
compByName[iace.NormalizeDEPublic(c.Name)] = c.ID
|
||||
}
|
||||
}
|
||||
|
||||
created := 0
|
||||
seenCat := make(map[string]bool)
|
||||
seenCatZone := make(map[string]bool)
|
||||
for _, mp := range matchOutput.MatchedPatterns {
|
||||
for _, cat := range mp.HazardCats {
|
||||
if seenCat[cat] {
|
||||
// Dedup by category + zone (allows multiple hazards per category at different zones)
|
||||
zoneKey := mp.ZoneDE
|
||||
if zoneKey == "" {
|
||||
zoneKey = mp.PatternID
|
||||
}
|
||||
dedupKey := cat + ":" + zoneKey
|
||||
if seenCatZone[dedupKey] {
|
||||
continue
|
||||
}
|
||||
seenCat[cat] = true
|
||||
seenCatZone[dedupKey] = true
|
||||
|
||||
name := mp.PatternName
|
||||
if name == "" {
|
||||
name = cat
|
||||
}
|
||||
// Append zone to name for specificity
|
||||
if mp.ZoneDE != "" && !containsSubstring(name, mp.ZoneDE) {
|
||||
name = name + " (" + mp.ZoneDE + ")"
|
||||
}
|
||||
|
||||
// Find matching component by zone name
|
||||
compID := defaultCompID
|
||||
if mp.ZoneDE != "" {
|
||||
zoneNorm := iace.NormalizeDEPublic(mp.ZoneDE)
|
||||
for cName, cID := range compByName {
|
||||
if containsSubstring(zoneNorm, cName) || containsSubstring(cName, zoneNorm) {
|
||||
compID = cID
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
hz, cerr := h.store.CreateHazard(ctx, iace.CreateHazardRequest{
|
||||
ProjectID: projectID,
|
||||
ComponentID: defaultCompID,
|
||||
ComponentID: compID,
|
||||
Name: name,
|
||||
Description: mp.ScenarioDE,
|
||||
Category: cat,
|
||||
|
||||
@@ -2,6 +2,7 @@ package handlers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
|
||||
"github.com/breakpilot/ai-compliance-sdk/internal/iace"
|
||||
"github.com/google/uuid"
|
||||
@@ -190,6 +191,14 @@ func extractIndustrySectorsFromMetadata(metadata json.RawMessage) []string {
|
||||
return result
|
||||
}
|
||||
|
||||
// containsSubstring checks if haystack contains needle (case-insensitive, normalized).
|
||||
func containsSubstring(haystack, needle string) bool {
|
||||
return strings.Contains(
|
||||
strings.ToLower(haystack),
|
||||
strings.ToLower(needle),
|
||||
)
|
||||
}
|
||||
|
||||
// findHazardForMeasureByCategory finds a matching hazard for a measure.
|
||||
func findHazardForMeasureByCategory(measureCat string, hazardsByCategory map[string]uuid.UUID) uuid.UUID {
|
||||
if id, ok := hazardsByCategory[measureCat]; ok {
|
||||
|
||||
Reference in New Issue
Block a user