Files
breakpilot-compliance/ai-compliance-sdk/internal/api/handlers/compliance_graph_handlers_test.go
T
Benjamin Admin 8609b696c9
CI / detect-changes (push) Successful in 12s
CI / branch-name (push) Has been skipped
CI / guardrail-integrity (push) Has been skipped
CI / secret-scan (push) Has been skipped
CI / dep-audit (push) Has been skipped
CI / sbom-scan (push) Has been skipped
CI / build-sha-integrity (push) Successful in 7s
CI / validate-canonical-controls (push) Successful in 5s
CI / loc-budget (push) Successful in 18s
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) Has been skipped
CI / test-go (push) Successful in 59s
CI / iace-gt-coverage (push) Successful in 19s
CI / test-python-backend (push) Has been skipped
CI / test-python-document-crawler (push) Has been skipped
CI / test-python-dsms-gateway (push) Has been skipped
fix(ucca): CM-7 repo_scan is required evidence for attack_surface_minimization
evidence_required lists only required:true rows; repo_scan was required:false so
attack_surface_minimization surfaced config_export alone. An attack-surface scan
IS required to evidence a minimized attack surface. Adds a test pinning the curated
evidence_required set per NIST obligation (the table test only checked control count).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-26 09:42:12 +02:00

134 lines
4.5 KiB
Go

package handlers
import (
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"github.com/gin-gonic/gin"
)
func newComplianceGraphTestRouter(t *testing.T) *gin.Engine {
t.Helper()
gin.SetMode(gin.TestMode)
h := NewComplianceGraphHandlers()
if err := h.LoadError(); err != nil {
t.Fatalf("compliance graph failed to load (candidate paths): %v", err)
}
r := gin.New()
h.RegisterRoutes(r.Group("/sdk/v1"))
return r
}
func getObligationStatus(t *testing.T, r *gin.Engine, query string) (int, cgStatusResponse) {
t.Helper()
w := httptest.NewRecorder()
req, _ := http.NewRequest(http.MethodGet, "/sdk/v1/compliance/obligation-status"+query, nil)
r.ServeHTTP(w, req)
var resp cgStatusResponse
if w.Code == http.StatusOK {
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
t.Fatalf("decode body %q: %v", w.Body.String(), err)
}
}
return w.Code, resp
}
func TestObligationStatus(t *testing.T) {
r := newComplianceGraphTestRouter(t)
tests := []struct {
name string
query string
wantHTTP int
wantOverall string
wantControls bool // expect >=1 control
}{
{"missing param -> 400", "", http.StatusBadRequest, "", false},
{"unknown id -> unknown_obligation", "?obligation_id=does_not_exist", http.StatusOK, "unknown_obligation", false},
{"mapped (OWASP V6) -> not_assessed", "?obligation_id=user_authentication_required", http.StatusOK, "not_assessed", true},
{"NIST adopted (SI-2) -> not_assessed", "?obligation_id=provide_security_updates", http.StatusOK, "not_assessed", true},
{"CORE attack_surface_minimization -> CM-7", "?obligation_id=attack_surface_minimization", http.StatusOK, "not_assessed", true},
{"CORE software_integrity_protection -> SI-7", "?obligation_id=software_integrity_protection", http.StatusOK, "not_assessed", true},
{"in registry, no control -> unmapped", "?obligation_id=sbom_creation", http.StatusOK, "unmapped", false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
code, resp := getObligationStatus(t, r, tt.query)
if code != tt.wantHTTP {
t.Fatalf("http %d, want %d", code, tt.wantHTTP)
}
if tt.wantHTTP != http.StatusOK {
return
}
if resp.OverallStatus != tt.wantOverall {
t.Errorf("overall_status=%q, want %q", resp.OverallStatus, tt.wantOverall)
}
if tt.wantControls && len(resp.Controls) == 0 {
t.Error("expected >=1 control")
}
if !tt.wantControls && len(resp.Controls) != 0 {
t.Errorf("expected 0 controls, got %d", len(resp.Controls))
}
if resp.CitationSpans != "pending" {
t.Errorf("citation_spans=%q, want pending", resp.CitationSpans)
}
})
}
}
// The MVP must NEVER auto-assert fulfillment: with no evidence collection wired, every required
// evidence is "missing" and the overall status stays "not_assessed".
func TestObligationStatus_NoFulfillmentClaim(t *testing.T) {
r := newComplianceGraphTestRouter(t)
code, resp := getObligationStatus(t, r, "?obligation_id=user_authentication_required")
if code != http.StatusOK {
t.Fatalf("http %d", code)
}
if resp.OverallStatus == "met" || resp.OverallStatus == "erfuellt" {
t.Fatalf("MVP must not assert fulfillment, got overall_status=%q", resp.OverallStatus)
}
for _, ctl := range resp.Controls {
if len(ctl.EvidenceRequired) > 0 && ctl.EvidenceStatus != "missing" {
t.Errorf("control %s/%s evidence_status=%q, want missing (no collection wired)", ctl.Framework, ctl.Control, ctl.EvidenceStatus)
}
}
}
// Pin the curated evidence_required set per NIST obligation. A required:false row silently
// drops from evidence_required, which the table test above (control-count only) would miss.
func TestObligationStatus_NISTEvidenceTypes(t *testing.T) {
r := newComplianceGraphTestRouter(t)
want := map[string][]string{
"attack_surface_minimization": {"config_export", "repo_scan"},
"software_integrity_protection": {"sbom", "config_export"},
"provide_security_updates": {"config_export", "test_report"},
}
for ob, exp := range want {
_, resp := getObligationStatus(t, r, "?obligation_id="+ob)
if len(resp.Controls) != 1 {
t.Fatalf("%s: want 1 control, got %d", ob, len(resp.Controls))
}
if got := resp.Controls[0].EvidenceRequired; !sameStringSet(got, exp) {
t.Errorf("%s evidence_required = %v, want %v", ob, got, exp)
}
}
}
func sameStringSet(a, b []string) bool {
if len(a) != len(b) {
return false
}
m := make(map[string]bool, len(a))
for _, x := range a {
m[x] = true
}
for _, x := range b {
if !m[x] {
return false
}
}
return true
}