merge: Feature-Module (Payment, BetrVG, FISA 702) in refakturierten main
Some checks failed
Build + Deploy / build-admin-compliance (push) Successful in 1m30s
Build + Deploy / build-backend-compliance (push) Successful in 13s
Build + Deploy / build-ai-sdk (push) Failing after 29s
Build + Deploy / build-developer-portal (push) Successful in 6s
Build + Deploy / build-tts (push) Successful in 6s
Build + Deploy / build-document-crawler (push) Successful in 6s
Build + Deploy / build-dsms-gateway (push) Successful in 6s
Build + Deploy / trigger-orca (push) Has been skipped
CI / branch-name (push) Has been skipped
CI / guardrail-integrity (push) Has been skipped
CI / loc-budget (push) Failing after 12s
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 2m18s
CI / dep-audit (push) Has been skipped
CI / sbom-scan (push) Has been skipped
CI / test-go (push) Failing after 29s
CI / test-python-backend (push) Successful in 34s
CI / test-python-document-crawler (push) Successful in 23s
CI / test-python-dsms-gateway (push) Successful in 19s
CI / validate-canonical-controls (push) Successful in 30s
Some checks failed
Build + Deploy / build-admin-compliance (push) Successful in 1m30s
Build + Deploy / build-backend-compliance (push) Successful in 13s
Build + Deploy / build-ai-sdk (push) Failing after 29s
Build + Deploy / build-developer-portal (push) Successful in 6s
Build + Deploy / build-tts (push) Successful in 6s
Build + Deploy / build-document-crawler (push) Successful in 6s
Build + Deploy / build-dsms-gateway (push) Successful in 6s
Build + Deploy / trigger-orca (push) Has been skipped
CI / branch-name (push) Has been skipped
CI / guardrail-integrity (push) Has been skipped
CI / loc-budget (push) Failing after 12s
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 2m18s
CI / dep-audit (push) Has been skipped
CI / sbom-scan (push) Has been skipped
CI / test-go (push) Failing after 29s
CI / test-python-backend (push) Successful in 34s
CI / test-python-document-crawler (push) Successful in 23s
CI / test-python-dsms-gateway (push) Successful in 19s
CI / validate-canonical-controls (push) Successful in 30s
Merged feature/fisa-702-drittland-risiko in den refakturierten main-Branch. Konflikte in 8 Dateien aufgelöst — neue Features in die aufgesplittete Modulstruktur integriert. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
290
ai-compliance-sdk/internal/api/handlers/payment_handlers.go
Normal file
290
ai-compliance-sdk/internal/api/handlers/payment_handlers.go
Normal file
@@ -0,0 +1,290 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
)
|
||||
|
||||
// PaymentHandlers handles payment compliance endpoints
|
||||
type PaymentHandlers struct {
|
||||
pool *pgxpool.Pool
|
||||
controls *PaymentControlLibrary
|
||||
}
|
||||
|
||||
// PaymentControlLibrary holds the control catalog
|
||||
type PaymentControlLibrary struct {
|
||||
Domains []PaymentDomain `json:"domains"`
|
||||
Controls []PaymentControl `json:"controls"`
|
||||
}
|
||||
|
||||
type PaymentDomain struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
}
|
||||
|
||||
type PaymentControl struct {
|
||||
ControlID string `json:"control_id"`
|
||||
Domain string `json:"domain"`
|
||||
Title string `json:"title"`
|
||||
Objective string `json:"objective"`
|
||||
CheckTarget string `json:"check_target"`
|
||||
Evidence []string `json:"evidence"`
|
||||
Automation string `json:"automation"`
|
||||
}
|
||||
|
||||
type PaymentAssessment struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
TenantID uuid.UUID `json:"tenant_id"`
|
||||
ProjectName string `json:"project_name"`
|
||||
TenderReference string `json:"tender_reference,omitempty"`
|
||||
CustomerName string `json:"customer_name,omitempty"`
|
||||
Description string `json:"description,omitempty"`
|
||||
SystemType string `json:"system_type,omitempty"`
|
||||
PaymentMethods json.RawMessage `json:"payment_methods,omitempty"`
|
||||
Protocols json.RawMessage `json:"protocols,omitempty"`
|
||||
TotalControls int `json:"total_controls"`
|
||||
ControlsPassed int `json:"controls_passed"`
|
||||
ControlsFailed int `json:"controls_failed"`
|
||||
ControlsPartial int `json:"controls_partial"`
|
||||
ControlsNA int `json:"controls_not_applicable"`
|
||||
ControlsUnchecked int `json:"controls_not_checked"`
|
||||
ComplianceScore float64 `json:"compliance_score"`
|
||||
Status string `json:"status"`
|
||||
ControlResults json.RawMessage `json:"control_results,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
CreatedBy string `json:"created_by,omitempty"`
|
||||
}
|
||||
|
||||
// NewPaymentHandlers creates payment handlers with loaded control library
|
||||
func NewPaymentHandlers(pool *pgxpool.Pool) *PaymentHandlers {
|
||||
lib := loadControlLibrary()
|
||||
return &PaymentHandlers{pool: pool, controls: lib}
|
||||
}
|
||||
|
||||
func loadControlLibrary() *PaymentControlLibrary {
|
||||
// Try to load from policies directory
|
||||
paths := []string{
|
||||
"policies/payment_controls_v1.json",
|
||||
"/app/policies/payment_controls_v1.json",
|
||||
}
|
||||
for _, p := range paths {
|
||||
data, err := os.ReadFile(p)
|
||||
if err != nil {
|
||||
// Try relative to executable
|
||||
execDir, _ := os.Executable()
|
||||
altPath := filepath.Join(filepath.Dir(execDir), p)
|
||||
data, err = os.ReadFile(altPath)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
}
|
||||
var lib PaymentControlLibrary
|
||||
if err := json.Unmarshal(data, &lib); err == nil {
|
||||
return &lib
|
||||
}
|
||||
}
|
||||
return &PaymentControlLibrary{}
|
||||
}
|
||||
|
||||
// GetControlLibrary returns the loaded control library (for tender matching)
|
||||
func (h *PaymentHandlers) GetControlLibrary() *PaymentControlLibrary {
|
||||
return h.controls
|
||||
}
|
||||
|
||||
// ListControls returns the control library
|
||||
func (h *PaymentHandlers) ListControls(c *gin.Context) {
|
||||
domain := c.Query("domain")
|
||||
automation := c.Query("automation")
|
||||
|
||||
controls := h.controls.Controls
|
||||
if domain != "" {
|
||||
var filtered []PaymentControl
|
||||
for _, ctrl := range controls {
|
||||
if ctrl.Domain == domain {
|
||||
filtered = append(filtered, ctrl)
|
||||
}
|
||||
}
|
||||
controls = filtered
|
||||
}
|
||||
if automation != "" {
|
||||
var filtered []PaymentControl
|
||||
for _, ctrl := range controls {
|
||||
if ctrl.Automation == automation {
|
||||
filtered = append(filtered, ctrl)
|
||||
}
|
||||
}
|
||||
controls = filtered
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"controls": controls,
|
||||
"domains": h.controls.Domains,
|
||||
"total": len(controls),
|
||||
})
|
||||
}
|
||||
|
||||
// CreateAssessment creates a new payment compliance assessment
|
||||
func (h *PaymentHandlers) CreateAssessment(c *gin.Context) {
|
||||
var req PaymentAssessment
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
tenantID, _ := uuid.Parse(c.GetHeader("X-Tenant-ID"))
|
||||
if tenantID == uuid.Nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "tenant ID required"})
|
||||
return
|
||||
}
|
||||
|
||||
req.ID = uuid.New()
|
||||
req.TenantID = tenantID
|
||||
req.Status = "draft"
|
||||
req.TotalControls = len(h.controls.Controls)
|
||||
req.ControlsUnchecked = req.TotalControls
|
||||
req.CreatedAt = time.Now()
|
||||
req.UpdatedAt = time.Now()
|
||||
|
||||
_, err := h.pool.Exec(c.Request.Context(), `
|
||||
INSERT INTO payment_compliance_assessments (
|
||||
id, tenant_id, project_name, tender_reference, customer_name, description,
|
||||
system_type, payment_methods, protocols,
|
||||
total_controls, controls_not_checked, status, created_by
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)`,
|
||||
req.ID, req.TenantID, req.ProjectName, req.TenderReference, req.CustomerName, req.Description,
|
||||
req.SystemType, req.PaymentMethods, req.Protocols,
|
||||
req.TotalControls, req.ControlsUnchecked, req.Status, req.CreatedBy,
|
||||
)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusCreated, req)
|
||||
}
|
||||
|
||||
// ListAssessments lists all payment assessments for a tenant
|
||||
func (h *PaymentHandlers) ListAssessments(c *gin.Context) {
|
||||
tenantID, _ := uuid.Parse(c.GetHeader("X-Tenant-ID"))
|
||||
if tenantID == uuid.Nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "tenant ID required"})
|
||||
return
|
||||
}
|
||||
|
||||
rows, err := h.pool.Query(c.Request.Context(), `
|
||||
SELECT id, tenant_id, project_name, tender_reference, customer_name,
|
||||
system_type, total_controls, controls_passed, controls_failed,
|
||||
controls_partial, controls_not_applicable, controls_not_checked,
|
||||
compliance_score, status, created_at, updated_at
|
||||
FROM payment_compliance_assessments
|
||||
WHERE tenant_id = $1
|
||||
ORDER BY created_at DESC`, tenantID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var assessments []PaymentAssessment
|
||||
for rows.Next() {
|
||||
var a PaymentAssessment
|
||||
rows.Scan(&a.ID, &a.TenantID, &a.ProjectName, &a.TenderReference, &a.CustomerName,
|
||||
&a.SystemType, &a.TotalControls, &a.ControlsPassed, &a.ControlsFailed,
|
||||
&a.ControlsPartial, &a.ControlsNA, &a.ControlsUnchecked,
|
||||
&a.ComplianceScore, &a.Status, &a.CreatedAt, &a.UpdatedAt)
|
||||
assessments = append(assessments, a)
|
||||
}
|
||||
if assessments == nil {
|
||||
assessments = []PaymentAssessment{}
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"assessments": assessments, "total": len(assessments)})
|
||||
}
|
||||
|
||||
// GetAssessment returns a single assessment with control results
|
||||
func (h *PaymentHandlers) GetAssessment(c *gin.Context) {
|
||||
id, err := uuid.Parse(c.Param("id"))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid ID"})
|
||||
return
|
||||
}
|
||||
|
||||
var a PaymentAssessment
|
||||
err = h.pool.QueryRow(c.Request.Context(), `
|
||||
SELECT id, tenant_id, project_name, tender_reference, customer_name, description,
|
||||
system_type, payment_methods, protocols,
|
||||
total_controls, controls_passed, controls_failed, controls_partial,
|
||||
controls_not_applicable, controls_not_checked, compliance_score,
|
||||
status, control_results, created_at, updated_at, created_by
|
||||
FROM payment_compliance_assessments WHERE id = $1`, id).Scan(
|
||||
&a.ID, &a.TenantID, &a.ProjectName, &a.TenderReference, &a.CustomerName, &a.Description,
|
||||
&a.SystemType, &a.PaymentMethods, &a.Protocols,
|
||||
&a.TotalControls, &a.ControlsPassed, &a.ControlsFailed, &a.ControlsPartial,
|
||||
&a.ControlsNA, &a.ControlsUnchecked, &a.ComplianceScore,
|
||||
&a.Status, &a.ControlResults, &a.CreatedAt, &a.UpdatedAt, &a.CreatedBy)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "assessment not found"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, a)
|
||||
}
|
||||
|
||||
// UpdateControlVerdict updates the verdict for a single control
|
||||
func (h *PaymentHandlers) UpdateControlVerdict(c *gin.Context) {
|
||||
id, err := uuid.Parse(c.Param("id"))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid ID"})
|
||||
return
|
||||
}
|
||||
|
||||
var body struct {
|
||||
ControlID string `json:"control_id"`
|
||||
Verdict string `json:"verdict"` // passed, failed, partial, na, unchecked
|
||||
Evidence string `json:"evidence,omitempty"`
|
||||
Notes string `json:"notes,omitempty"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&body); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// Update the control_results JSONB and recalculate scores
|
||||
_, err = h.pool.Exec(c.Request.Context(), `
|
||||
WITH updated AS (
|
||||
SELECT id,
|
||||
COALESCE(control_results, '[]'::jsonb) AS existing_results
|
||||
FROM payment_compliance_assessments WHERE id = $1
|
||||
)
|
||||
UPDATE payment_compliance_assessments SET
|
||||
control_results = (
|
||||
SELECT jsonb_agg(
|
||||
CASE WHEN elem->>'control_id' = $2 THEN
|
||||
jsonb_build_object('control_id', $2, 'verdict', $3, 'evidence', $4, 'notes', $5)
|
||||
ELSE elem END
|
||||
) FROM updated, jsonb_array_elements(
|
||||
CASE WHEN existing_results @> jsonb_build_array(jsonb_build_object('control_id', $2))
|
||||
THEN existing_results
|
||||
ELSE existing_results || jsonb_build_array(jsonb_build_object('control_id', $2, 'verdict', $3, 'evidence', $4, 'notes', $5))
|
||||
END
|
||||
) AS elem
|
||||
),
|
||||
updated_at = NOW()
|
||||
WHERE id = $1`,
|
||||
id, body.ControlID, body.Verdict, body.Evidence, body.Notes)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"status": "updated", "control_id": body.ControlID, "verdict": body.Verdict})
|
||||
}
|
||||
220
ai-compliance-sdk/internal/api/handlers/registration_handlers.go
Normal file
220
ai-compliance-sdk/internal/api/handlers/registration_handlers.go
Normal file
@@ -0,0 +1,220 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/breakpilot/ai-compliance-sdk/internal/ucca"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// RegistrationHandlers handles EU AI Database registration endpoints
|
||||
type RegistrationHandlers struct {
|
||||
store *ucca.RegistrationStore
|
||||
uccaStore *ucca.Store
|
||||
}
|
||||
|
||||
// NewRegistrationHandlers creates new registration handlers
|
||||
func NewRegistrationHandlers(store *ucca.RegistrationStore, uccaStore *ucca.Store) *RegistrationHandlers {
|
||||
return &RegistrationHandlers{store: store, uccaStore: uccaStore}
|
||||
}
|
||||
|
||||
// Create creates a new registration
|
||||
func (h *RegistrationHandlers) Create(c *gin.Context) {
|
||||
var reg ucca.AIRegistration
|
||||
if err := c.ShouldBindJSON(®); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
tenantID, _ := uuid.Parse(c.GetHeader("X-Tenant-ID"))
|
||||
if tenantID == uuid.Nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "tenant ID required"})
|
||||
return
|
||||
}
|
||||
reg.TenantID = tenantID
|
||||
|
||||
if reg.SystemName == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "system_name required"})
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.store.Create(c.Request.Context(), ®); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create registration: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusCreated, reg)
|
||||
}
|
||||
|
||||
// List lists all registrations for the tenant
|
||||
func (h *RegistrationHandlers) List(c *gin.Context) {
|
||||
tenantID, _ := uuid.Parse(c.GetHeader("X-Tenant-ID"))
|
||||
if tenantID == uuid.Nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "tenant ID required"})
|
||||
return
|
||||
}
|
||||
|
||||
registrations, err := h.store.List(c.Request.Context(), tenantID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to list registrations: " + err.Error()})
|
||||
return
|
||||
}
|
||||
if registrations == nil {
|
||||
registrations = []ucca.AIRegistration{}
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"registrations": registrations, "total": len(registrations)})
|
||||
}
|
||||
|
||||
// Get returns a single registration
|
||||
func (h *RegistrationHandlers) Get(c *gin.Context) {
|
||||
id, err := uuid.Parse(c.Param("id"))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid ID"})
|
||||
return
|
||||
}
|
||||
|
||||
reg, err := h.store.GetByID(c.Request.Context(), id)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Registration not found"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, reg)
|
||||
}
|
||||
|
||||
// Update updates a registration
|
||||
func (h *RegistrationHandlers) Update(c *gin.Context) {
|
||||
id, err := uuid.Parse(c.Param("id"))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid ID"})
|
||||
return
|
||||
}
|
||||
|
||||
existing, err := h.store.GetByID(c.Request.Context(), id)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Registration not found"})
|
||||
return
|
||||
}
|
||||
|
||||
var updates ucca.AIRegistration
|
||||
if err := c.ShouldBindJSON(&updates); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// Merge updates into existing
|
||||
updates.ID = existing.ID
|
||||
updates.TenantID = existing.TenantID
|
||||
updates.CreatedAt = existing.CreatedAt
|
||||
|
||||
if err := h.store.Update(c.Request.Context(), &updates); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, updates)
|
||||
}
|
||||
|
||||
// UpdateStatus changes the registration status
|
||||
func (h *RegistrationHandlers) UpdateStatus(c *gin.Context) {
|
||||
id, err := uuid.Parse(c.Param("id"))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid ID"})
|
||||
return
|
||||
}
|
||||
|
||||
var body struct {
|
||||
Status string `json:"status"`
|
||||
SubmittedBy string `json:"submitted_by"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&body); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request"})
|
||||
return
|
||||
}
|
||||
|
||||
validStatuses := map[string]bool{
|
||||
"draft": true, "ready": true, "submitted": true,
|
||||
"registered": true, "update_required": true, "withdrawn": true,
|
||||
}
|
||||
if !validStatuses[body.Status] {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid status. Valid: draft, ready, submitted, registered, update_required, withdrawn"})
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.store.UpdateStatus(c.Request.Context(), id, body.Status, body.SubmittedBy); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update status: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"id": id, "status": body.Status})
|
||||
}
|
||||
|
||||
// Prefill creates a registration pre-filled from a UCCA assessment
|
||||
func (h *RegistrationHandlers) Prefill(c *gin.Context) {
|
||||
assessmentID, err := uuid.Parse(c.Param("assessment_id"))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid assessment ID"})
|
||||
return
|
||||
}
|
||||
|
||||
tenantID, _ := uuid.Parse(c.GetHeader("X-Tenant-ID"))
|
||||
if tenantID == uuid.Nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "tenant ID required"})
|
||||
return
|
||||
}
|
||||
|
||||
// Load UCCA assessment
|
||||
assessment, err := h.uccaStore.GetAssessment(c.Request.Context(), assessmentID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Assessment not found"})
|
||||
return
|
||||
}
|
||||
|
||||
// Pre-fill registration from assessment intake
|
||||
intake := assessment.Intake
|
||||
|
||||
reg := ucca.AIRegistration{
|
||||
TenantID: tenantID,
|
||||
SystemName: intake.Title,
|
||||
SystemDescription: intake.UseCaseText,
|
||||
IntendedPurpose: intake.UseCaseText,
|
||||
RiskClassification: string(assessment.RiskLevel),
|
||||
GPAIClassification: "none",
|
||||
RegistrationStatus: "draft",
|
||||
UCCAAssessmentID: &assessmentID,
|
||||
}
|
||||
|
||||
// Map domain to readable text
|
||||
if intake.Domain != "" {
|
||||
reg.IntendedPurpose = string(intake.Domain) + ": " + intake.UseCaseText
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, reg)
|
||||
}
|
||||
|
||||
// Export generates the EU AI Database submission JSON
|
||||
func (h *RegistrationHandlers) Export(c *gin.Context) {
|
||||
id, err := uuid.Parse(c.Param("id"))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid ID"})
|
||||
return
|
||||
}
|
||||
|
||||
reg, err := h.store.GetByID(c.Request.Context(), id)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Registration not found"})
|
||||
return
|
||||
}
|
||||
|
||||
exportJSON := h.store.BuildExportJSON(reg)
|
||||
|
||||
// Save export data to DB
|
||||
reg.ExportData = exportJSON
|
||||
h.store.Update(c.Request.Context(), reg)
|
||||
|
||||
c.Header("Content-Type", "application/json")
|
||||
c.Header("Content-Disposition", "attachment; filename=eu_ai_registration_"+reg.SystemName+".json")
|
||||
c.Data(http.StatusOK, "application/json", exportJSON)
|
||||
}
|
||||
557
ai-compliance-sdk/internal/api/handlers/tender_handlers.go
Normal file
557
ai-compliance-sdk/internal/api/handlers/tender_handlers.go
Normal file
@@ -0,0 +1,557 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
)
|
||||
|
||||
// TenderHandlers handles tender upload and requirement extraction
|
||||
type TenderHandlers struct {
|
||||
pool *pgxpool.Pool
|
||||
controls *PaymentControlLibrary
|
||||
}
|
||||
|
||||
// TenderAnalysis represents a tender document analysis
|
||||
type TenderAnalysis struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
TenantID uuid.UUID `json:"tenant_id"`
|
||||
FileName string `json:"file_name"`
|
||||
FileSize int64 `json:"file_size"`
|
||||
ProjectName string `json:"project_name"`
|
||||
CustomerName string `json:"customer_name,omitempty"`
|
||||
Status string `json:"status"` // uploaded, extracting, extracted, matched, completed
|
||||
Requirements []ExtractedReq `json:"requirements,omitempty"`
|
||||
MatchResults []MatchResult `json:"match_results,omitempty"`
|
||||
TotalRequirements int `json:"total_requirements"`
|
||||
MatchedCount int `json:"matched_count"`
|
||||
UnmatchedCount int `json:"unmatched_count"`
|
||||
PartialCount int `json:"partial_count"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
// ExtractedReq represents a single requirement extracted from a tender document
|
||||
type ExtractedReq struct {
|
||||
ReqID string `json:"req_id"`
|
||||
Text string `json:"text"`
|
||||
SourcePage int `json:"source_page,omitempty"`
|
||||
SourceSection string `json:"source_section,omitempty"`
|
||||
ObligationLevel string `json:"obligation_level"` // MUST, SHALL, SHOULD, MAY
|
||||
TechnicalDomain string `json:"technical_domain"` // crypto, logging, payment_flow, etc.
|
||||
CheckTarget string `json:"check_target"` // code, system, config, process, certificate
|
||||
Confidence float64 `json:"confidence"`
|
||||
}
|
||||
|
||||
// MatchResult represents the matching of a requirement to controls
|
||||
type MatchResult struct {
|
||||
ReqID string `json:"req_id"`
|
||||
ReqText string `json:"req_text"`
|
||||
ObligationLevel string `json:"obligation_level"`
|
||||
MatchedControls []ControlMatch `json:"matched_controls"`
|
||||
Verdict string `json:"verdict"` // matched, partial, unmatched
|
||||
GapDescription string `json:"gap_description,omitempty"`
|
||||
}
|
||||
|
||||
// ControlMatch represents a single control match for a requirement
|
||||
type ControlMatch struct {
|
||||
ControlID string `json:"control_id"`
|
||||
Title string `json:"title"`
|
||||
Relevance float64 `json:"relevance"` // 0-1
|
||||
CheckTarget string `json:"check_target"`
|
||||
}
|
||||
|
||||
// NewTenderHandlers creates tender handlers
|
||||
func NewTenderHandlers(pool *pgxpool.Pool, controls *PaymentControlLibrary) *TenderHandlers {
|
||||
return &TenderHandlers{pool: pool, controls: controls}
|
||||
}
|
||||
|
||||
// Upload handles tender document upload
|
||||
func (h *TenderHandlers) Upload(c *gin.Context) {
|
||||
tenantID, _ := uuid.Parse(c.GetHeader("X-Tenant-ID"))
|
||||
if tenantID == uuid.Nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "tenant ID required"})
|
||||
return
|
||||
}
|
||||
|
||||
file, header, err := c.Request.FormFile("file")
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "file required"})
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
projectName := c.PostForm("project_name")
|
||||
if projectName == "" {
|
||||
projectName = header.Filename
|
||||
}
|
||||
customerName := c.PostForm("customer_name")
|
||||
|
||||
// Read file content
|
||||
content, err := io.ReadAll(file)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to read file"})
|
||||
return
|
||||
}
|
||||
|
||||
// Store analysis record
|
||||
analysisID := uuid.New()
|
||||
now := time.Now()
|
||||
|
||||
_, err = h.pool.Exec(c.Request.Context(), `
|
||||
INSERT INTO tender_analyses (
|
||||
id, tenant_id, file_name, file_size, file_content,
|
||||
project_name, customer_name, status, created_at, updated_at
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7, 'uploaded', $8, $9)`,
|
||||
analysisID, tenantID, header.Filename, header.Size, content,
|
||||
projectName, customerName, now, now,
|
||||
)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to store: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusCreated, gin.H{
|
||||
"id": analysisID,
|
||||
"file_name": header.Filename,
|
||||
"file_size": header.Size,
|
||||
"project_name": projectName,
|
||||
"status": "uploaded",
|
||||
"message": "Dokument hochgeladen. Starte Analyse mit POST /extract.",
|
||||
})
|
||||
}
|
||||
|
||||
// Extract extracts requirements from an uploaded tender document using LLM
|
||||
func (h *TenderHandlers) Extract(c *gin.Context) {
|
||||
id, err := uuid.Parse(c.Param("id"))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid ID"})
|
||||
return
|
||||
}
|
||||
|
||||
// Get file content
|
||||
var fileContent []byte
|
||||
var fileName string
|
||||
err = h.pool.QueryRow(c.Request.Context(), `
|
||||
SELECT file_content, file_name FROM tender_analyses WHERE id = $1`, id,
|
||||
).Scan(&fileContent, &fileName)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "analysis not found"})
|
||||
return
|
||||
}
|
||||
|
||||
// Update status
|
||||
h.pool.Exec(c.Request.Context(), `
|
||||
UPDATE tender_analyses SET status = 'extracting', updated_at = NOW() WHERE id = $1`, id)
|
||||
|
||||
// Extract text (simple: treat as text for now, PDF extraction would use embedding-service)
|
||||
text := string(fileContent)
|
||||
|
||||
// Use LLM to extract requirements
|
||||
requirements := h.extractRequirementsWithLLM(c.Request.Context(), text)
|
||||
|
||||
// Store results
|
||||
reqJSON, _ := json.Marshal(requirements)
|
||||
h.pool.Exec(c.Request.Context(), `
|
||||
UPDATE tender_analyses SET
|
||||
status = 'extracted',
|
||||
requirements = $2,
|
||||
total_requirements = $3,
|
||||
updated_at = NOW()
|
||||
WHERE id = $1`, id, reqJSON, len(requirements))
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"id": id,
|
||||
"status": "extracted",
|
||||
"requirements": requirements,
|
||||
"total": len(requirements),
|
||||
})
|
||||
}
|
||||
|
||||
// Match matches extracted requirements against the control library
|
||||
func (h *TenderHandlers) Match(c *gin.Context) {
|
||||
id, err := uuid.Parse(c.Param("id"))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid ID"})
|
||||
return
|
||||
}
|
||||
|
||||
// Get requirements
|
||||
var reqJSON json.RawMessage
|
||||
err = h.pool.QueryRow(c.Request.Context(), `
|
||||
SELECT requirements FROM tender_analyses WHERE id = $1`, id,
|
||||
).Scan(&reqJSON)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "analysis not found"})
|
||||
return
|
||||
}
|
||||
|
||||
var requirements []ExtractedReq
|
||||
json.Unmarshal(reqJSON, &requirements)
|
||||
|
||||
// Match each requirement against controls
|
||||
var results []MatchResult
|
||||
matched, unmatched, partial := 0, 0, 0
|
||||
|
||||
for _, req := range requirements {
|
||||
matches := h.findMatchingControls(req)
|
||||
result := MatchResult{
|
||||
ReqID: req.ReqID,
|
||||
ReqText: req.Text,
|
||||
ObligationLevel: req.ObligationLevel,
|
||||
MatchedControls: matches,
|
||||
}
|
||||
|
||||
if len(matches) == 0 {
|
||||
result.Verdict = "unmatched"
|
||||
result.GapDescription = "Kein passender Control gefunden — manueller Review erforderlich"
|
||||
unmatched++
|
||||
} else if matches[0].Relevance >= 0.7 {
|
||||
result.Verdict = "matched"
|
||||
matched++
|
||||
} else {
|
||||
result.Verdict = "partial"
|
||||
result.GapDescription = "Teilweise Abdeckung — Control deckt Anforderung nicht vollstaendig ab"
|
||||
partial++
|
||||
}
|
||||
|
||||
results = append(results, result)
|
||||
}
|
||||
|
||||
// Store results
|
||||
resultsJSON, _ := json.Marshal(results)
|
||||
h.pool.Exec(c.Request.Context(), `
|
||||
UPDATE tender_analyses SET
|
||||
status = 'matched',
|
||||
match_results = $2,
|
||||
matched_count = $3,
|
||||
unmatched_count = $4,
|
||||
partial_count = $5,
|
||||
updated_at = NOW()
|
||||
WHERE id = $1`, id, resultsJSON, matched, unmatched, partial)
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"id": id,
|
||||
"status": "matched",
|
||||
"results": results,
|
||||
"matched": matched,
|
||||
"unmatched": unmatched,
|
||||
"partial": partial,
|
||||
"total": len(requirements),
|
||||
})
|
||||
}
|
||||
|
||||
// ListAnalyses lists all tender analyses for a tenant
|
||||
func (h *TenderHandlers) ListAnalyses(c *gin.Context) {
|
||||
tenantID, _ := uuid.Parse(c.GetHeader("X-Tenant-ID"))
|
||||
if tenantID == uuid.Nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "tenant ID required"})
|
||||
return
|
||||
}
|
||||
|
||||
rows, err := h.pool.Query(c.Request.Context(), `
|
||||
SELECT id, tenant_id, file_name, file_size, project_name, customer_name,
|
||||
status, total_requirements, matched_count, unmatched_count, partial_count,
|
||||
created_at, updated_at
|
||||
FROM tender_analyses
|
||||
WHERE tenant_id = $1
|
||||
ORDER BY created_at DESC`, tenantID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var analyses []TenderAnalysis
|
||||
for rows.Next() {
|
||||
var a TenderAnalysis
|
||||
rows.Scan(&a.ID, &a.TenantID, &a.FileName, &a.FileSize, &a.ProjectName, &a.CustomerName,
|
||||
&a.Status, &a.TotalRequirements, &a.MatchedCount, &a.UnmatchedCount, &a.PartialCount,
|
||||
&a.CreatedAt, &a.UpdatedAt)
|
||||
analyses = append(analyses, a)
|
||||
}
|
||||
if analyses == nil {
|
||||
analyses = []TenderAnalysis{}
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"analyses": analyses, "total": len(analyses)})
|
||||
}
|
||||
|
||||
// GetAnalysis returns a single analysis with all details
|
||||
func (h *TenderHandlers) GetAnalysis(c *gin.Context) {
|
||||
id, err := uuid.Parse(c.Param("id"))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid ID"})
|
||||
return
|
||||
}
|
||||
|
||||
var a TenderAnalysis
|
||||
var reqJSON, matchJSON json.RawMessage
|
||||
err = h.pool.QueryRow(c.Request.Context(), `
|
||||
SELECT id, tenant_id, file_name, file_size, project_name, customer_name,
|
||||
status, requirements, match_results,
|
||||
total_requirements, matched_count, unmatched_count, partial_count,
|
||||
created_at, updated_at
|
||||
FROM tender_analyses WHERE id = $1`, id).Scan(
|
||||
&a.ID, &a.TenantID, &a.FileName, &a.FileSize, &a.ProjectName, &a.CustomerName,
|
||||
&a.Status, &reqJSON, &matchJSON,
|
||||
&a.TotalRequirements, &a.MatchedCount, &a.UnmatchedCount, &a.PartialCount,
|
||||
&a.CreatedAt, &a.UpdatedAt)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
|
||||
return
|
||||
}
|
||||
|
||||
if reqJSON != nil {
|
||||
json.Unmarshal(reqJSON, &a.Requirements)
|
||||
}
|
||||
if matchJSON != nil {
|
||||
json.Unmarshal(matchJSON, &a.MatchResults)
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, a)
|
||||
}
|
||||
|
||||
// --- Internal helpers ---
|
||||
|
||||
func (h *TenderHandlers) extractRequirementsWithLLM(ctx context.Context, text string) []ExtractedReq {
|
||||
// Try Anthropic API for requirement extraction
|
||||
apiKey := os.Getenv("ANTHROPIC_API_KEY")
|
||||
if apiKey == "" {
|
||||
// Fallback: simple keyword-based extraction
|
||||
return h.extractRequirementsKeyword(text)
|
||||
}
|
||||
|
||||
prompt := fmt.Sprintf(`Analysiere das folgende Ausschreibungsdokument und extrahiere alle technischen Anforderungen.
|
||||
|
||||
Fuer jede Anforderung gib zurueck:
|
||||
- req_id: fortlaufende ID (REQ-001, REQ-002, ...)
|
||||
- text: die Anforderung als kurzer Satz
|
||||
- obligation_level: MUST, SHALL, SHOULD oder MAY
|
||||
- technical_domain: eines von: payment_flow, logging, crypto, api_security, terminal_comm, firmware, reporting, access_control, error_handling, build_deploy
|
||||
- check_target: eines von: code, system, config, process, certificate
|
||||
|
||||
Antworte NUR mit JSON Array. Keine Erklaerung.
|
||||
|
||||
Dokument:
|
||||
%s`, text[:min(len(text), 15000)])
|
||||
|
||||
body := map[string]interface{}{
|
||||
"model": "claude-haiku-4-5-20251001",
|
||||
"max_tokens": 4096,
|
||||
"messages": []map[string]string{{"role": "user", "content": prompt}},
|
||||
}
|
||||
bodyJSON, _ := json.Marshal(body)
|
||||
|
||||
req, _ := http.NewRequestWithContext(ctx, "POST", "https://api.anthropic.com/v1/messages", strings.NewReader(string(bodyJSON)))
|
||||
req.Header.Set("x-api-key", apiKey)
|
||||
req.Header.Set("anthropic-version", "2023-06-01")
|
||||
req.Header.Set("content-type", "application/json")
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil || resp.StatusCode != 200 {
|
||||
return h.extractRequirementsKeyword(text)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var result struct {
|
||||
Content []struct {
|
||||
Text string `json:"text"`
|
||||
} `json:"content"`
|
||||
}
|
||||
json.NewDecoder(resp.Body).Decode(&result)
|
||||
|
||||
if len(result.Content) == 0 {
|
||||
return h.extractRequirementsKeyword(text)
|
||||
}
|
||||
|
||||
// Parse LLM response
|
||||
responseText := result.Content[0].Text
|
||||
// Find JSON array in response
|
||||
start := strings.Index(responseText, "[")
|
||||
end := strings.LastIndex(responseText, "]")
|
||||
if start < 0 || end < 0 {
|
||||
return h.extractRequirementsKeyword(text)
|
||||
}
|
||||
|
||||
var reqs []ExtractedReq
|
||||
if err := json.Unmarshal([]byte(responseText[start:end+1]), &reqs); err != nil {
|
||||
return h.extractRequirementsKeyword(text)
|
||||
}
|
||||
|
||||
// Set confidence for LLM-extracted requirements
|
||||
for i := range reqs {
|
||||
reqs[i].Confidence = 0.8
|
||||
}
|
||||
|
||||
return reqs
|
||||
}
|
||||
|
||||
func (h *TenderHandlers) extractRequirementsKeyword(text string) []ExtractedReq {
|
||||
// Simple keyword-based extraction as fallback
|
||||
keywords := map[string]string{
|
||||
"muss": "MUST",
|
||||
"muessen": "MUST",
|
||||
"ist sicherzustellen": "MUST",
|
||||
"soll": "SHOULD",
|
||||
"sollte": "SHOULD",
|
||||
"kann": "MAY",
|
||||
"wird gefordert": "MUST",
|
||||
"nachzuweisen": "MUST",
|
||||
"zertifiziert": "MUST",
|
||||
}
|
||||
|
||||
var reqs []ExtractedReq
|
||||
lines := strings.Split(text, "\n")
|
||||
reqNum := 1
|
||||
|
||||
for _, line := range lines {
|
||||
line = strings.TrimSpace(line)
|
||||
if len(line) < 20 || len(line) > 500 {
|
||||
continue
|
||||
}
|
||||
|
||||
for keyword, level := range keywords {
|
||||
if strings.Contains(strings.ToLower(line), keyword) {
|
||||
reqs = append(reqs, ExtractedReq{
|
||||
ReqID: fmt.Sprintf("REQ-%03d", reqNum),
|
||||
Text: line,
|
||||
ObligationLevel: level,
|
||||
TechnicalDomain: inferDomain(line),
|
||||
CheckTarget: inferCheckTarget(line),
|
||||
Confidence: 0.5,
|
||||
})
|
||||
reqNum++
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return reqs
|
||||
}
|
||||
|
||||
func (h *TenderHandlers) findMatchingControls(req ExtractedReq) []ControlMatch {
|
||||
var matches []ControlMatch
|
||||
|
||||
reqLower := strings.ToLower(req.Text + " " + req.TechnicalDomain)
|
||||
|
||||
for _, ctrl := range h.controls.Controls {
|
||||
titleLower := strings.ToLower(ctrl.Title + " " + ctrl.Objective)
|
||||
relevance := calculateRelevance(reqLower, titleLower, req.TechnicalDomain, ctrl.Domain)
|
||||
|
||||
if relevance > 0.3 {
|
||||
matches = append(matches, ControlMatch{
|
||||
ControlID: ctrl.ControlID,
|
||||
Title: ctrl.Title,
|
||||
Relevance: relevance,
|
||||
CheckTarget: ctrl.CheckTarget,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by relevance (simple bubble sort for small lists)
|
||||
for i := 0; i < len(matches); i++ {
|
||||
for j := i + 1; j < len(matches); j++ {
|
||||
if matches[j].Relevance > matches[i].Relevance {
|
||||
matches[i], matches[j] = matches[j], matches[i]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Return top 5
|
||||
if len(matches) > 5 {
|
||||
matches = matches[:5]
|
||||
}
|
||||
|
||||
return matches
|
||||
}
|
||||
|
||||
func calculateRelevance(reqText, ctrlText, reqDomain, ctrlDomain string) float64 {
|
||||
score := 0.0
|
||||
|
||||
// Domain match bonus
|
||||
domainMap := map[string]string{
|
||||
"payment_flow": "PAY",
|
||||
"logging": "LOG",
|
||||
"crypto": "CRYPTO",
|
||||
"api_security": "API",
|
||||
"terminal_comm": "TERM",
|
||||
"firmware": "FW",
|
||||
"reporting": "REP",
|
||||
"access_control": "ACC",
|
||||
"error_handling": "ERR",
|
||||
"build_deploy": "BLD",
|
||||
}
|
||||
|
||||
if mapped, ok := domainMap[reqDomain]; ok && mapped == ctrlDomain {
|
||||
score += 0.4
|
||||
}
|
||||
|
||||
// Keyword overlap
|
||||
reqWords := strings.Fields(reqText)
|
||||
for _, word := range reqWords {
|
||||
if len(word) > 3 && strings.Contains(ctrlText, word) {
|
||||
score += 0.1
|
||||
}
|
||||
}
|
||||
|
||||
if score > 1.0 {
|
||||
score = 1.0
|
||||
}
|
||||
return score
|
||||
}
|
||||
|
||||
func inferDomain(text string) string {
|
||||
textLower := strings.ToLower(text)
|
||||
domainKeywords := map[string][]string{
|
||||
"payment_flow": {"zahlung", "transaktion", "buchung", "payment", "betrag"},
|
||||
"logging": {"log", "protokoll", "audit", "nachvollzieh"},
|
||||
"crypto": {"verschlüssel", "schlüssel", "krypto", "tls", "ssl", "hsm", "pin"},
|
||||
"api_security": {"api", "schnittstelle", "authentifiz", "autorisier"},
|
||||
"terminal_comm": {"terminal", "zvt", "opi", "gerät", "kontaktlos", "nfc"},
|
||||
"firmware": {"firmware", "update", "signatur", "boot"},
|
||||
"reporting": {"bericht", "report", "abrechnung", "export", "abgleich"},
|
||||
"access_control": {"zugang", "benutzer", "passwort", "rolle", "berechtigung"},
|
||||
"error_handling": {"fehler", "ausfall", "recovery", "offline", "störung"},
|
||||
"build_deploy": {"build", "deploy", "release", "ci", "pipeline"},
|
||||
}
|
||||
|
||||
for domain, keywords := range domainKeywords {
|
||||
for _, kw := range keywords {
|
||||
if strings.Contains(textLower, kw) {
|
||||
return domain
|
||||
}
|
||||
}
|
||||
}
|
||||
return "general"
|
||||
}
|
||||
|
||||
func inferCheckTarget(text string) string {
|
||||
textLower := strings.ToLower(text)
|
||||
if strings.Contains(textLower, "zertifik") || strings.Contains(textLower, "zulassung") {
|
||||
return "certificate"
|
||||
}
|
||||
if strings.Contains(textLower, "prozess") || strings.Contains(textLower, "verfahren") {
|
||||
return "process"
|
||||
}
|
||||
if strings.Contains(textLower, "konfigur") {
|
||||
return "config"
|
||||
}
|
||||
return "code"
|
||||
}
|
||||
|
||||
func min(a, b int) int {
|
||||
if a < b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
305
ai-compliance-sdk/internal/ucca/betrvg_test.go
Normal file
305
ai-compliance-sdk/internal/ucca/betrvg_test.go
Normal file
@@ -0,0 +1,305 @@
|
||||
package ucca
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// ============================================================================
|
||||
// BetrVG Conflict Score Tests
|
||||
// ============================================================================
|
||||
|
||||
func TestCalculateBetrvgConflictScore_NoEmployeeData(t *testing.T) {
|
||||
root := getProjectRoot(t)
|
||||
policyPath := filepath.Join(root, "policies", "ucca_policy_v1.yaml")
|
||||
engine, err := NewPolicyEngineFromPath(policyPath)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create policy engine: %v", err)
|
||||
}
|
||||
|
||||
intake := &UseCaseIntake{
|
||||
UseCaseText: "Chatbot fuer Kunden-FAQ",
|
||||
Domain: DomainUtilities,
|
||||
DataTypes: DataTypes{
|
||||
PersonalData: false,
|
||||
PublicData: true,
|
||||
},
|
||||
}
|
||||
|
||||
result := engine.Evaluate(intake)
|
||||
|
||||
if result.BetrvgConflictScore != 0 {
|
||||
t.Errorf("Expected BetrvgConflictScore 0 for non-employee case, got %d", result.BetrvgConflictScore)
|
||||
}
|
||||
if result.BetrvgConsultationRequired {
|
||||
t.Error("Expected BetrvgConsultationRequired=false for non-employee case")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCalculateBetrvgConflictScore_EmployeeMonitoring(t *testing.T) {
|
||||
root := getProjectRoot(t)
|
||||
policyPath := filepath.Join(root, "policies", "ucca_policy_v1.yaml")
|
||||
engine, err := NewPolicyEngineFromPath(policyPath)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create policy engine: %v", err)
|
||||
}
|
||||
|
||||
intake := &UseCaseIntake{
|
||||
UseCaseText: "Teams Analytics mit Nutzungsstatistiken pro Mitarbeiter",
|
||||
Domain: DomainIT,
|
||||
DataTypes: DataTypes{
|
||||
PersonalData: true,
|
||||
EmployeeData: true,
|
||||
},
|
||||
EmployeeMonitoring: true,
|
||||
}
|
||||
|
||||
result := engine.Evaluate(intake)
|
||||
|
||||
// employee_data(+10) + employee_monitoring(+20) + not_consulted(+5) = 35
|
||||
if result.BetrvgConflictScore < 30 {
|
||||
t.Errorf("Expected BetrvgConflictScore >= 30 for employee monitoring, got %d", result.BetrvgConflictScore)
|
||||
}
|
||||
if !result.BetrvgConsultationRequired {
|
||||
t.Error("Expected BetrvgConsultationRequired=true for employee monitoring")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCalculateBetrvgConflictScore_HRDecisionSupport(t *testing.T) {
|
||||
root := getProjectRoot(t)
|
||||
policyPath := filepath.Join(root, "policies", "ucca_policy_v1.yaml")
|
||||
engine, err := NewPolicyEngineFromPath(policyPath)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create policy engine: %v", err)
|
||||
}
|
||||
|
||||
intake := &UseCaseIntake{
|
||||
UseCaseText: "KI-gestuetztes Bewerber-Screening",
|
||||
Domain: DomainHR,
|
||||
DataTypes: DataTypes{
|
||||
PersonalData: true,
|
||||
EmployeeData: true,
|
||||
},
|
||||
EmployeeMonitoring: true,
|
||||
HRDecisionSupport: true,
|
||||
Automation: "fully_automated",
|
||||
Outputs: Outputs{
|
||||
Rankings: true,
|
||||
},
|
||||
}
|
||||
|
||||
result := engine.Evaluate(intake)
|
||||
|
||||
// employee_data(+10) + monitoring(+20) + hr(+20) + rankings(+10) + fully_auto(+10) + not_consulted(+5) = 75
|
||||
if result.BetrvgConflictScore < 70 {
|
||||
t.Errorf("Expected BetrvgConflictScore >= 70 for HR+monitoring+automated, got %d", result.BetrvgConflictScore)
|
||||
}
|
||||
if !result.BetrvgConsultationRequired {
|
||||
t.Error("Expected BetrvgConsultationRequired=true")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCalculateBetrvgConflictScore_ConsultedReducesScore(t *testing.T) {
|
||||
root := getProjectRoot(t)
|
||||
policyPath := filepath.Join(root, "policies", "ucca_policy_v1.yaml")
|
||||
engine, err := NewPolicyEngineFromPath(policyPath)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create policy engine: %v", err)
|
||||
}
|
||||
|
||||
// Same as above but works council consulted
|
||||
intakeNotConsulted := &UseCaseIntake{
|
||||
UseCaseText: "Teams mit Nutzungsstatistiken",
|
||||
Domain: DomainIT,
|
||||
DataTypes: DataTypes{
|
||||
PersonalData: true,
|
||||
EmployeeData: true,
|
||||
},
|
||||
EmployeeMonitoring: true,
|
||||
WorksCouncilConsulted: false,
|
||||
}
|
||||
|
||||
intakeConsulted := &UseCaseIntake{
|
||||
UseCaseText: "Teams mit Nutzungsstatistiken",
|
||||
Domain: DomainIT,
|
||||
DataTypes: DataTypes{
|
||||
PersonalData: true,
|
||||
EmployeeData: true,
|
||||
},
|
||||
EmployeeMonitoring: true,
|
||||
WorksCouncilConsulted: true,
|
||||
}
|
||||
|
||||
resultNot := engine.Evaluate(intakeNotConsulted)
|
||||
resultYes := engine.Evaluate(intakeConsulted)
|
||||
|
||||
if resultYes.BetrvgConflictScore >= resultNot.BetrvgConflictScore {
|
||||
t.Errorf("Expected consulted score (%d) < not-consulted score (%d)",
|
||||
resultYes.BetrvgConflictScore, resultNot.BetrvgConflictScore)
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// BetrVG Escalation Tests
|
||||
// ============================================================================
|
||||
|
||||
func TestEscalation_BetrvgHighConflict_E3(t *testing.T) {
|
||||
trigger := DefaultEscalationTrigger()
|
||||
|
||||
result := &AssessmentResult{
|
||||
Feasibility: FeasibilityCONDITIONAL,
|
||||
RiskLevel: RiskLevelMEDIUM,
|
||||
RiskScore: 45,
|
||||
BetrvgConflictScore: 80,
|
||||
BetrvgConsultationRequired: true,
|
||||
Intake: UseCaseIntake{
|
||||
WorksCouncilConsulted: false,
|
||||
},
|
||||
TriggeredRules: []TriggeredRule{
|
||||
{Code: "R-WARN-001", Severity: "WARN"},
|
||||
},
|
||||
}
|
||||
|
||||
level, reason := trigger.DetermineEscalationLevel(result)
|
||||
|
||||
if level != EscalationLevelE3 {
|
||||
t.Errorf("Expected E3 for high BR conflict without consultation, got %s (reason: %s)", level, reason)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEscalation_BetrvgMediumConflict_E2(t *testing.T) {
|
||||
trigger := DefaultEscalationTrigger()
|
||||
|
||||
result := &AssessmentResult{
|
||||
Feasibility: FeasibilityCONDITIONAL,
|
||||
RiskLevel: RiskLevelLOW,
|
||||
RiskScore: 25,
|
||||
BetrvgConflictScore: 55,
|
||||
BetrvgConsultationRequired: true,
|
||||
Intake: UseCaseIntake{
|
||||
WorksCouncilConsulted: false,
|
||||
},
|
||||
TriggeredRules: []TriggeredRule{
|
||||
{Code: "R-WARN-001", Severity: "WARN"},
|
||||
},
|
||||
}
|
||||
|
||||
level, reason := trigger.DetermineEscalationLevel(result)
|
||||
|
||||
if level != EscalationLevelE2 {
|
||||
t.Errorf("Expected E2 for medium BR conflict without consultation, got %s (reason: %s)", level, reason)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEscalation_BetrvgConsulted_NoEscalation(t *testing.T) {
|
||||
trigger := DefaultEscalationTrigger()
|
||||
|
||||
result := &AssessmentResult{
|
||||
Feasibility: FeasibilityYES,
|
||||
RiskLevel: RiskLevelLOW,
|
||||
RiskScore: 15,
|
||||
BetrvgConflictScore: 55,
|
||||
BetrvgConsultationRequired: true,
|
||||
Intake: UseCaseIntake{
|
||||
WorksCouncilConsulted: true,
|
||||
},
|
||||
TriggeredRules: []TriggeredRule{},
|
||||
}
|
||||
|
||||
level, _ := trigger.DetermineEscalationLevel(result)
|
||||
|
||||
// With consultation done and low risk, should not escalate for BR reasons
|
||||
if level == EscalationLevelE3 {
|
||||
t.Error("Should not escalate to E3 when works council is consulted")
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// BetrVG V2 Obligations Loading Test
|
||||
// ============================================================================
|
||||
|
||||
func TestBetrvgV2_LoadsFromManifest(t *testing.T) {
|
||||
root := getProjectRoot(t)
|
||||
v2Dir := filepath.Join(root, "policies", "obligations", "v2")
|
||||
|
||||
// Check file exists
|
||||
betrvgPath := filepath.Join(v2Dir, "betrvg_v2.json")
|
||||
if _, err := os.Stat(betrvgPath); os.IsNotExist(err) {
|
||||
t.Fatal("betrvg_v2.json not found in policies/obligations/v2/")
|
||||
}
|
||||
|
||||
// Load all v2 regulations
|
||||
regs, err := LoadAllV2Regulations()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to load v2 regulations: %v", err)
|
||||
}
|
||||
|
||||
betrvg, ok := regs["betrvg"]
|
||||
if !ok {
|
||||
t.Fatal("betrvg not found in loaded regulations")
|
||||
}
|
||||
|
||||
if betrvg.Regulation != "betrvg" {
|
||||
t.Errorf("Expected regulation 'betrvg', got '%s'", betrvg.Regulation)
|
||||
}
|
||||
|
||||
if len(betrvg.Obligations) < 10 {
|
||||
t.Errorf("Expected at least 10 BetrVG obligations, got %d", len(betrvg.Obligations))
|
||||
}
|
||||
|
||||
// Check first obligation has correct structure
|
||||
obl := betrvg.Obligations[0]
|
||||
if obl.ID != "BETRVG-OBL-001" {
|
||||
t.Errorf("Expected first obligation ID 'BETRVG-OBL-001', got '%s'", obl.ID)
|
||||
}
|
||||
if len(obl.LegalBasis) == 0 {
|
||||
t.Error("Expected legal basis for first obligation")
|
||||
}
|
||||
if obl.LegalBasis[0].Norm != "BetrVG" {
|
||||
t.Errorf("Expected norm 'BetrVG', got '%s'", obl.LegalBasis[0].Norm)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBetrvgApplicability_Germany(t *testing.T) {
|
||||
regs, err := LoadAllV2Regulations()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to load v2 regulations: %v", err)
|
||||
}
|
||||
|
||||
betrvgReg := regs["betrvg"]
|
||||
module := NewJSONRegulationModule(betrvgReg)
|
||||
|
||||
// German company with 50 employees — should be applicable
|
||||
factsDE := &UnifiedFacts{
|
||||
Organization: OrganizationFacts{
|
||||
Country: "DE",
|
||||
EmployeeCount: 50,
|
||||
},
|
||||
}
|
||||
if !module.IsApplicable(factsDE) {
|
||||
t.Error("BetrVG should be applicable for German company with 50 employees")
|
||||
}
|
||||
|
||||
// US company — should NOT be applicable
|
||||
factsUS := &UnifiedFacts{
|
||||
Organization: OrganizationFacts{
|
||||
Country: "US",
|
||||
EmployeeCount: 50,
|
||||
},
|
||||
}
|
||||
if module.IsApplicable(factsUS) {
|
||||
t.Error("BetrVG should NOT be applicable for US company")
|
||||
}
|
||||
|
||||
// German company with 3 employees — should NOT be applicable (threshold 5)
|
||||
factsSmall := &UnifiedFacts{
|
||||
Organization: OrganizationFacts{
|
||||
Country: "DE",
|
||||
EmployeeCount: 3,
|
||||
},
|
||||
}
|
||||
if module.IsApplicable(factsSmall) {
|
||||
t.Error("BetrVG should NOT be applicable for company with < 5 employees")
|
||||
}
|
||||
}
|
||||
325
ai-compliance-sdk/internal/ucca/decision_tree_engine.go
Normal file
325
ai-compliance-sdk/internal/ucca/decision_tree_engine.go
Normal file
@@ -0,0 +1,325 @@
|
||||
package ucca
|
||||
|
||||
// ============================================================================
|
||||
// AI Act Decision Tree Engine
|
||||
// ============================================================================
|
||||
//
|
||||
// Two-axis classification:
|
||||
// Axis 1 (Q1–Q7): High-Risk classification based on Annex III
|
||||
// Axis 2 (Q8–Q12): GPAI classification based on Art. 51–56
|
||||
//
|
||||
// Deterministic evaluation — no LLM involved.
|
||||
//
|
||||
// ============================================================================
|
||||
|
||||
// Question IDs
|
||||
const (
|
||||
Q1 = "Q1" // Uses AI?
|
||||
Q2 = "Q2" // Biometric identification?
|
||||
Q3 = "Q3" // Critical infrastructure?
|
||||
Q4 = "Q4" // Education / employment / HR?
|
||||
Q5 = "Q5" // Essential services (credit, insurance)?
|
||||
Q6 = "Q6" // Law enforcement / migration / justice?
|
||||
Q7 = "Q7" // Autonomous decisions with legal effect?
|
||||
Q8 = "Q8" // Foundation Model / GPAI?
|
||||
Q9 = "Q9" // Generates content (text, image, code, audio)?
|
||||
Q10 = "Q10" // Trained with >10^25 FLOP?
|
||||
Q11 = "Q11" // Model provided as API/service for third parties?
|
||||
Q12 = "Q12" // Significant EU market penetration?
|
||||
)
|
||||
|
||||
// BuildDecisionTreeDefinition returns the full decision tree structure for the frontend
|
||||
func BuildDecisionTreeDefinition() *DecisionTreeDefinition {
|
||||
return &DecisionTreeDefinition{
|
||||
ID: "ai_act_two_axis",
|
||||
Name: "AI Act Zwei-Achsen-Klassifikation",
|
||||
Version: "1.0.0",
|
||||
Questions: []DecisionTreeQuestion{
|
||||
// === Axis 1: High-Risk (Annex III) ===
|
||||
{
|
||||
ID: Q1,
|
||||
Axis: "high_risk",
|
||||
Question: "Setzt Ihr System KI-Technologie ein?",
|
||||
Description: "KI im Sinne des AI Act umfasst maschinelles Lernen, logik- und wissensbasierte Ansätze sowie statistische Methoden, die für eine gegebene Reihe von Zielen Ergebnisse wie Inhalte, Vorhersagen, Empfehlungen oder Entscheidungen erzeugen.",
|
||||
ArticleRef: "Art. 3 Nr. 1",
|
||||
},
|
||||
{
|
||||
ID: Q2,
|
||||
Axis: "high_risk",
|
||||
Question: "Wird das System für biometrische Identifikation oder Kategorisierung natürlicher Personen verwendet?",
|
||||
Description: "Dazu zählen Gesichtserkennung, Stimmerkennung, Fingerabdruck-Analyse, Gangerkennung oder andere biometrische Merkmale zur Identifikation oder Kategorisierung.",
|
||||
ArticleRef: "Anhang III Nr. 1",
|
||||
SkipIf: Q1,
|
||||
},
|
||||
{
|
||||
ID: Q3,
|
||||
Axis: "high_risk",
|
||||
Question: "Wird das System in kritischer Infrastruktur eingesetzt (Energie, Verkehr, Wasser, digitale Infrastruktur)?",
|
||||
Description: "Betrifft KI-Systeme als Sicherheitskomponenten in der Verwaltung und dem Betrieb kritischer digitaler Infrastruktur, des Straßenverkehrs oder der Wasser-, Gas-, Heizungs- oder Stromversorgung.",
|
||||
ArticleRef: "Anhang III Nr. 2",
|
||||
SkipIf: Q1,
|
||||
},
|
||||
{
|
||||
ID: Q4,
|
||||
Axis: "high_risk",
|
||||
Question: "Betrifft das System Bildung, Beschäftigung oder Personalmanagement?",
|
||||
Description: "KI zur Festlegung des Zugangs zu Bildungseinrichtungen, Bewertung von Prüfungsleistungen, Bewerbungsauswahl, Beförderungsentscheidungen oder Überwachung von Arbeitnehmern.",
|
||||
ArticleRef: "Anhang III Nr. 3–4",
|
||||
SkipIf: Q1,
|
||||
},
|
||||
{
|
||||
ID: Q5,
|
||||
Axis: "high_risk",
|
||||
Question: "Betrifft das System den Zugang zu wesentlichen Diensten (Kreditvergabe, Versicherung, öffentliche Leistungen)?",
|
||||
Description: "KI zur Bonitätsbewertung, Risikobewertung bei Versicherungen, Bewertung der Anspruchsberechtigung für öffentliche Unterstützungsleistungen oder Notdienste.",
|
||||
ArticleRef: "Anhang III Nr. 5",
|
||||
SkipIf: Q1,
|
||||
},
|
||||
{
|
||||
ID: Q6,
|
||||
Axis: "high_risk",
|
||||
Question: "Wird das System in Strafverfolgung, Migration, Asyl oder Justiz eingesetzt?",
|
||||
Description: "KI für Lügendetektoren, Beweisbewertung, Rückfallprognose, Asylentscheidungen, Grenzkontrolle, Risikobewertung bei Migration oder Unterstützung der Rechtspflege.",
|
||||
ArticleRef: "Anhang III Nr. 6–8",
|
||||
SkipIf: Q1,
|
||||
},
|
||||
{
|
||||
ID: Q7,
|
||||
Axis: "high_risk",
|
||||
Question: "Trifft das System autonome Entscheidungen mit rechtlicher Wirkung für natürliche Personen?",
|
||||
Description: "Entscheidungen, die Rechtsverhältnisse begründen, ändern oder aufheben, z.B. Kreditablehnungen, Kündigungen, Sozialleistungsentscheidungen — ohne menschliche Überprüfung im Einzelfall.",
|
||||
ArticleRef: "Art. 22 DSGVO / Art. 14 AI Act",
|
||||
SkipIf: Q1,
|
||||
},
|
||||
|
||||
// === Axis 2: GPAI (Art. 51–56) ===
|
||||
{
|
||||
ID: Q8,
|
||||
Axis: "gpai",
|
||||
Question: "Stellst du ein KI-Modell fuer Dritte bereit (API / Plattform / SDK), das fuer viele verschiedene Zwecke einsetzbar ist?",
|
||||
Description: "GPAI-Pflichten (Art. 51-56) gelten fuer den Modellanbieter, nicht den API-Nutzer. Wenn du nur eine API nutzt (z.B. OpenAI, Claude), bist du kein GPAI-Anbieter. GPAI-Anbieter ist, wer ein Modell trainiert/fine-tuned und Dritten zur Verfuegung stellt. Beispiele: GPT, Claude, LLaMA, Gemini, Stable Diffusion.",
|
||||
ArticleRef: "Art. 3 Nr. 63 / Art. 51",
|
||||
},
|
||||
{
|
||||
ID: Q9,
|
||||
Axis: "gpai",
|
||||
Question: "Kann das System Inhalte generieren (Text, Bild, Code, Audio, Video)?",
|
||||
Description: "Generative KI erzeugt neue Inhalte auf Basis von Eingaben — dazu zählen Chatbots, Bild-/Videogeneratoren, Code-Assistenten, Sprachsynthese und ähnliche Systeme.",
|
||||
ArticleRef: "Art. 50 / Art. 52",
|
||||
SkipIf: Q8,
|
||||
},
|
||||
{
|
||||
ID: Q10,
|
||||
Axis: "gpai",
|
||||
Question: "Wurde das Modell mit mehr als 10²⁵ FLOP trainiert oder hat es gleichwertige Fähigkeiten?",
|
||||
Description: "GPAI-Modelle mit einem kumulativen Rechenaufwand von mehr als 10²⁵ Gleitkommaoperationen gelten als Modelle mit systemischem Risiko (Art. 51 Abs. 2).",
|
||||
ArticleRef: "Art. 51 Abs. 2",
|
||||
SkipIf: Q8,
|
||||
},
|
||||
{
|
||||
ID: Q11,
|
||||
Axis: "gpai",
|
||||
Question: "Wird das Modell als API oder Service für Dritte bereitgestellt?",
|
||||
Description: "Stellen Sie das Modell anderen Unternehmen oder Entwicklern zur Nutzung bereit (API, SaaS, Plattform-Integration)?",
|
||||
ArticleRef: "Art. 53",
|
||||
SkipIf: Q8,
|
||||
},
|
||||
{
|
||||
ID: Q12,
|
||||
Axis: "gpai",
|
||||
Question: "Hat das Modell eine signifikante Marktdurchdringung in der EU (>10.000 registrierte Geschäftsnutzer)?",
|
||||
Description: "Modelle mit hoher Marktdurchdringung können auch ohne 10²⁵ FLOP als systemisches Risiko eingestuft werden, wenn die EU-Kommission dies feststellt.",
|
||||
ArticleRef: "Art. 51 Abs. 3",
|
||||
SkipIf: Q8,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// EvaluateDecisionTree evaluates the answers and returns the combined result
|
||||
func EvaluateDecisionTree(req *DecisionTreeEvalRequest) *DecisionTreeResult {
|
||||
result := &DecisionTreeResult{
|
||||
SystemName: req.SystemName,
|
||||
SystemDescription: req.SystemDescription,
|
||||
Answers: req.Answers,
|
||||
}
|
||||
|
||||
// Evaluate Axis 1: High-Risk
|
||||
result.HighRiskResult = evaluateHighRiskAxis(req.Answers)
|
||||
|
||||
// Evaluate Axis 2: GPAI
|
||||
result.GPAIResult = evaluateGPAIAxis(req.Answers)
|
||||
|
||||
// Combine obligations and articles
|
||||
result.CombinedObligations = combineObligations(result.HighRiskResult, result.GPAIResult)
|
||||
result.ApplicableArticles = combineArticles(result.HighRiskResult, result.GPAIResult)
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// evaluateHighRiskAxis determines the AI Act risk level from Q1–Q7
|
||||
func evaluateHighRiskAxis(answers map[string]DecisionTreeAnswer) AIActRiskLevel {
|
||||
// Q1: Uses AI at all?
|
||||
if !answerIsYes(answers, Q1) {
|
||||
return AIActNotApplicable
|
||||
}
|
||||
|
||||
// Q2–Q6: Annex III high-risk categories
|
||||
if answerIsYes(answers, Q2) || answerIsYes(answers, Q3) ||
|
||||
answerIsYes(answers, Q4) || answerIsYes(answers, Q5) ||
|
||||
answerIsYes(answers, Q6) {
|
||||
return AIActHighRisk
|
||||
}
|
||||
|
||||
// Q7: Autonomous decisions with legal effect
|
||||
if answerIsYes(answers, Q7) {
|
||||
return AIActHighRisk
|
||||
}
|
||||
|
||||
// AI is used but no high-risk category triggered
|
||||
return AIActMinimalRisk
|
||||
}
|
||||
|
||||
// evaluateGPAIAxis determines the GPAI classification from Q8–Q12
|
||||
func evaluateGPAIAxis(answers map[string]DecisionTreeAnswer) GPAIClassification {
|
||||
gpai := GPAIClassification{
|
||||
Category: GPAICategoryNone,
|
||||
ApplicableArticles: []string{},
|
||||
Obligations: []string{},
|
||||
}
|
||||
|
||||
// Q8: Is GPAI?
|
||||
if !answerIsYes(answers, Q8) {
|
||||
return gpai
|
||||
}
|
||||
|
||||
gpai.IsGPAI = true
|
||||
gpai.Category = GPAICategoryStandard
|
||||
gpai.ApplicableArticles = append(gpai.ApplicableArticles, "Art. 51", "Art. 53")
|
||||
gpai.Obligations = append(gpai.Obligations,
|
||||
"Technische Dokumentation erstellen (Art. 53 Abs. 1a)",
|
||||
"Informationen für nachgelagerte Anbieter bereitstellen (Art. 53 Abs. 1b)",
|
||||
"Urheberrechtsrichtlinie einhalten (Art. 53 Abs. 1c)",
|
||||
"Trainingsdaten-Zusammenfassung veröffentlichen (Art. 53 Abs. 1d)",
|
||||
)
|
||||
|
||||
// Q9: Generative AI — adds transparency obligations
|
||||
if answerIsYes(answers, Q9) {
|
||||
gpai.ApplicableArticles = append(gpai.ApplicableArticles, "Art. 50")
|
||||
gpai.Obligations = append(gpai.Obligations,
|
||||
"KI-generierte Inhalte kennzeichnen (Art. 50 Abs. 2)",
|
||||
"Maschinenlesbare Kennzeichnung synthetischer Inhalte (Art. 50 Abs. 2)",
|
||||
)
|
||||
}
|
||||
|
||||
// Q10: Systemic risk threshold (>10^25 FLOP)
|
||||
if answerIsYes(answers, Q10) {
|
||||
gpai.IsSystemicRisk = true
|
||||
gpai.Category = GPAICategorySystemic
|
||||
gpai.ApplicableArticles = append(gpai.ApplicableArticles, "Art. 55")
|
||||
gpai.Obligations = append(gpai.Obligations,
|
||||
"Modellbewertung nach Stand der Technik durchführen (Art. 55 Abs. 1a)",
|
||||
"Systemische Risiken bewerten und mindern (Art. 55 Abs. 1b)",
|
||||
"Schwerwiegende Vorfälle melden (Art. 55 Abs. 1c)",
|
||||
"Angemessenes Cybersicherheitsniveau gewährleisten (Art. 55 Abs. 1d)",
|
||||
)
|
||||
}
|
||||
|
||||
// Q11: API/Service provider — additional downstream obligations
|
||||
if answerIsYes(answers, Q11) {
|
||||
gpai.Obligations = append(gpai.Obligations,
|
||||
"Downstream-Informationspflichten erfüllen (Art. 53 Abs. 1b)",
|
||||
)
|
||||
}
|
||||
|
||||
// Q12: Significant market penetration — potential systemic risk
|
||||
if answerIsYes(answers, Q12) && !gpai.IsSystemicRisk {
|
||||
// EU Commission can designate as systemic risk
|
||||
gpai.ApplicableArticles = append(gpai.ApplicableArticles, "Art. 51 Abs. 3")
|
||||
gpai.Obligations = append(gpai.Obligations,
|
||||
"Achtung: EU-Kommission kann GPAI mit hoher Marktdurchdringung als systemisches Risiko einstufen (Art. 51 Abs. 3)",
|
||||
)
|
||||
}
|
||||
|
||||
return gpai
|
||||
}
|
||||
|
||||
// combineObligations merges obligations from both axes
|
||||
func combineObligations(highRisk AIActRiskLevel, gpai GPAIClassification) []string {
|
||||
var obligations []string
|
||||
|
||||
// High-Risk obligations
|
||||
switch highRisk {
|
||||
case AIActHighRisk:
|
||||
obligations = append(obligations,
|
||||
"Risikomanagementsystem einrichten (Art. 9)",
|
||||
"Daten-Governance sicherstellen (Art. 10)",
|
||||
"Technische Dokumentation erstellen (Art. 11)",
|
||||
"Protokollierungsfunktion implementieren (Art. 12)",
|
||||
"Transparenz und Nutzerinformation (Art. 13)",
|
||||
"Menschliche Aufsicht ermöglichen (Art. 14)",
|
||||
"Genauigkeit, Robustheit und Cybersicherheit (Art. 15)",
|
||||
"EU-Datenbank-Registrierung (Art. 49)",
|
||||
)
|
||||
case AIActMinimalRisk:
|
||||
obligations = append(obligations,
|
||||
"Freiwillige Verhaltenskodizes empfohlen (Art. 95)",
|
||||
)
|
||||
case AIActNotApplicable:
|
||||
// No obligations
|
||||
}
|
||||
|
||||
// GPAI obligations
|
||||
obligations = append(obligations, gpai.Obligations...)
|
||||
|
||||
// Universal obligation for all AI users
|
||||
if highRisk != AIActNotApplicable {
|
||||
obligations = append(obligations,
|
||||
"KI-Kompetenz sicherstellen (Art. 4)",
|
||||
"Verbotene Praktiken vermeiden (Art. 5)",
|
||||
)
|
||||
}
|
||||
|
||||
return obligations
|
||||
}
|
||||
|
||||
// combineArticles merges applicable articles from both axes
|
||||
func combineArticles(highRisk AIActRiskLevel, gpai GPAIClassification) []string {
|
||||
articles := map[string]bool{}
|
||||
|
||||
// Universal
|
||||
if highRisk != AIActNotApplicable {
|
||||
articles["Art. 4"] = true
|
||||
articles["Art. 5"] = true
|
||||
}
|
||||
|
||||
// High-Risk
|
||||
switch highRisk {
|
||||
case AIActHighRisk:
|
||||
for _, a := range []string{"Art. 9", "Art. 10", "Art. 11", "Art. 12", "Art. 13", "Art. 14", "Art. 15", "Art. 26", "Art. 49"} {
|
||||
articles[a] = true
|
||||
}
|
||||
case AIActMinimalRisk:
|
||||
articles["Art. 95"] = true
|
||||
}
|
||||
|
||||
// GPAI
|
||||
for _, a := range gpai.ApplicableArticles {
|
||||
articles[a] = true
|
||||
}
|
||||
|
||||
var result []string
|
||||
for a := range articles {
|
||||
result = append(result, a)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// answerIsYes checks if a question was answered with "yes" (true)
|
||||
func answerIsYes(answers map[string]DecisionTreeAnswer, questionID string) bool {
|
||||
a, ok := answers[questionID]
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
return a.Value
|
||||
}
|
||||
420
ai-compliance-sdk/internal/ucca/decision_tree_engine_test.go
Normal file
420
ai-compliance-sdk/internal/ucca/decision_tree_engine_test.go
Normal file
@@ -0,0 +1,420 @@
|
||||
package ucca
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestBuildDecisionTreeDefinition_ReturnsValidTree(t *testing.T) {
|
||||
tree := BuildDecisionTreeDefinition()
|
||||
|
||||
if tree == nil {
|
||||
t.Fatal("Expected non-nil tree definition")
|
||||
}
|
||||
if tree.ID != "ai_act_two_axis" {
|
||||
t.Errorf("Expected ID 'ai_act_two_axis', got '%s'", tree.ID)
|
||||
}
|
||||
if tree.Version != "1.0.0" {
|
||||
t.Errorf("Expected version '1.0.0', got '%s'", tree.Version)
|
||||
}
|
||||
if len(tree.Questions) != 12 {
|
||||
t.Errorf("Expected 12 questions, got %d", len(tree.Questions))
|
||||
}
|
||||
|
||||
// Check axis distribution
|
||||
hrCount := 0
|
||||
gpaiCount := 0
|
||||
for _, q := range tree.Questions {
|
||||
switch q.Axis {
|
||||
case "high_risk":
|
||||
hrCount++
|
||||
case "gpai":
|
||||
gpaiCount++
|
||||
default:
|
||||
t.Errorf("Unexpected axis '%s' for question %s", q.Axis, q.ID)
|
||||
}
|
||||
}
|
||||
if hrCount != 7 {
|
||||
t.Errorf("Expected 7 high_risk questions, got %d", hrCount)
|
||||
}
|
||||
if gpaiCount != 5 {
|
||||
t.Errorf("Expected 5 gpai questions, got %d", gpaiCount)
|
||||
}
|
||||
|
||||
// Check all questions have required fields
|
||||
for _, q := range tree.Questions {
|
||||
if q.ID == "" {
|
||||
t.Error("Question has empty ID")
|
||||
}
|
||||
if q.Question == "" {
|
||||
t.Errorf("Question %s has empty question text", q.ID)
|
||||
}
|
||||
if q.Description == "" {
|
||||
t.Errorf("Question %s has empty description", q.ID)
|
||||
}
|
||||
if q.ArticleRef == "" {
|
||||
t.Errorf("Question %s has empty article_ref", q.ID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestEvaluateDecisionTree_NotApplicable(t *testing.T) {
|
||||
// Q1=No → AI Act not applicable
|
||||
req := &DecisionTreeEvalRequest{
|
||||
SystemName: "Test System",
|
||||
Answers: map[string]DecisionTreeAnswer{
|
||||
Q1: {QuestionID: Q1, Value: false},
|
||||
},
|
||||
}
|
||||
|
||||
result := EvaluateDecisionTree(req)
|
||||
|
||||
if result.HighRiskResult != AIActNotApplicable {
|
||||
t.Errorf("Expected not_applicable, got %s", result.HighRiskResult)
|
||||
}
|
||||
if result.GPAIResult.IsGPAI {
|
||||
t.Error("Expected GPAI to be false when Q8 is not answered")
|
||||
}
|
||||
if result.SystemName != "Test System" {
|
||||
t.Errorf("Expected system name 'Test System', got '%s'", result.SystemName)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEvaluateDecisionTree_MinimalRisk(t *testing.T) {
|
||||
// Q1=Yes, Q2-Q7=No → minimal risk
|
||||
req := &DecisionTreeEvalRequest{
|
||||
SystemName: "Simple Tool",
|
||||
Answers: map[string]DecisionTreeAnswer{
|
||||
Q1: {QuestionID: Q1, Value: true},
|
||||
Q2: {QuestionID: Q2, Value: false},
|
||||
Q3: {QuestionID: Q3, Value: false},
|
||||
Q4: {QuestionID: Q4, Value: false},
|
||||
Q5: {QuestionID: Q5, Value: false},
|
||||
Q6: {QuestionID: Q6, Value: false},
|
||||
Q7: {QuestionID: Q7, Value: false},
|
||||
Q8: {QuestionID: Q8, Value: false},
|
||||
},
|
||||
}
|
||||
|
||||
result := EvaluateDecisionTree(req)
|
||||
|
||||
if result.HighRiskResult != AIActMinimalRisk {
|
||||
t.Errorf("Expected minimal_risk, got %s", result.HighRiskResult)
|
||||
}
|
||||
if result.GPAIResult.IsGPAI {
|
||||
t.Error("Expected GPAI to be false")
|
||||
}
|
||||
if result.GPAIResult.Category != GPAICategoryNone {
|
||||
t.Errorf("Expected GPAI category 'none', got '%s'", result.GPAIResult.Category)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEvaluateDecisionTree_HighRisk_Biometric(t *testing.T) {
|
||||
// Q1=Yes, Q2=Yes → high risk (biometric)
|
||||
req := &DecisionTreeEvalRequest{
|
||||
SystemName: "Face Recognition",
|
||||
Answers: map[string]DecisionTreeAnswer{
|
||||
Q1: {QuestionID: Q1, Value: true},
|
||||
Q2: {QuestionID: Q2, Value: true},
|
||||
Q3: {QuestionID: Q3, Value: false},
|
||||
Q4: {QuestionID: Q4, Value: false},
|
||||
Q5: {QuestionID: Q5, Value: false},
|
||||
Q6: {QuestionID: Q6, Value: false},
|
||||
Q7: {QuestionID: Q7, Value: false},
|
||||
},
|
||||
}
|
||||
|
||||
result := EvaluateDecisionTree(req)
|
||||
|
||||
if result.HighRiskResult != AIActHighRisk {
|
||||
t.Errorf("Expected high_risk, got %s", result.HighRiskResult)
|
||||
}
|
||||
|
||||
// Should have high-risk obligations
|
||||
if len(result.CombinedObligations) == 0 {
|
||||
t.Error("Expected non-empty obligations for high-risk system")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEvaluateDecisionTree_HighRisk_CriticalInfrastructure(t *testing.T) {
|
||||
// Q1=Yes, Q3=Yes → high risk (critical infrastructure)
|
||||
req := &DecisionTreeEvalRequest{
|
||||
SystemName: "Energy Grid AI",
|
||||
Answers: map[string]DecisionTreeAnswer{
|
||||
Q1: {QuestionID: Q1, Value: true},
|
||||
Q2: {QuestionID: Q2, Value: false},
|
||||
Q3: {QuestionID: Q3, Value: true},
|
||||
Q4: {QuestionID: Q4, Value: false},
|
||||
Q5: {QuestionID: Q5, Value: false},
|
||||
Q6: {QuestionID: Q6, Value: false},
|
||||
Q7: {QuestionID: Q7, Value: false},
|
||||
},
|
||||
}
|
||||
|
||||
result := EvaluateDecisionTree(req)
|
||||
|
||||
if result.HighRiskResult != AIActHighRisk {
|
||||
t.Errorf("Expected high_risk, got %s", result.HighRiskResult)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEvaluateDecisionTree_HighRisk_Education(t *testing.T) {
|
||||
// Q1=Yes, Q4=Yes → high risk (education/employment)
|
||||
req := &DecisionTreeEvalRequest{
|
||||
SystemName: "Exam Grading AI",
|
||||
Answers: map[string]DecisionTreeAnswer{
|
||||
Q1: {QuestionID: Q1, Value: true},
|
||||
Q2: {QuestionID: Q2, Value: false},
|
||||
Q3: {QuestionID: Q3, Value: false},
|
||||
Q4: {QuestionID: Q4, Value: true},
|
||||
},
|
||||
}
|
||||
|
||||
result := EvaluateDecisionTree(req)
|
||||
|
||||
if result.HighRiskResult != AIActHighRisk {
|
||||
t.Errorf("Expected high_risk, got %s", result.HighRiskResult)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEvaluateDecisionTree_HighRisk_AutonomousDecisions(t *testing.T) {
|
||||
// Q1=Yes, Q7=Yes → high risk (autonomous decisions)
|
||||
req := &DecisionTreeEvalRequest{
|
||||
SystemName: "Credit Scoring AI",
|
||||
Answers: map[string]DecisionTreeAnswer{
|
||||
Q1: {QuestionID: Q1, Value: true},
|
||||
Q2: {QuestionID: Q2, Value: false},
|
||||
Q3: {QuestionID: Q3, Value: false},
|
||||
Q4: {QuestionID: Q4, Value: false},
|
||||
Q5: {QuestionID: Q5, Value: false},
|
||||
Q6: {QuestionID: Q6, Value: false},
|
||||
Q7: {QuestionID: Q7, Value: true},
|
||||
},
|
||||
}
|
||||
|
||||
result := EvaluateDecisionTree(req)
|
||||
|
||||
if result.HighRiskResult != AIActHighRisk {
|
||||
t.Errorf("Expected high_risk, got %s", result.HighRiskResult)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEvaluateDecisionTree_GPAI_Standard(t *testing.T) {
|
||||
// Q8=Yes, Q10=No → GPAI standard
|
||||
req := &DecisionTreeEvalRequest{
|
||||
SystemName: "Custom LLM",
|
||||
Answers: map[string]DecisionTreeAnswer{
|
||||
Q1: {QuestionID: Q1, Value: true},
|
||||
Q8: {QuestionID: Q8, Value: true},
|
||||
Q9: {QuestionID: Q9, Value: true},
|
||||
Q10: {QuestionID: Q10, Value: false},
|
||||
Q11: {QuestionID: Q11, Value: false},
|
||||
Q12: {QuestionID: Q12, Value: false},
|
||||
},
|
||||
}
|
||||
|
||||
result := EvaluateDecisionTree(req)
|
||||
|
||||
if !result.GPAIResult.IsGPAI {
|
||||
t.Error("Expected IsGPAI to be true")
|
||||
}
|
||||
if result.GPAIResult.Category != GPAICategoryStandard {
|
||||
t.Errorf("Expected category 'standard', got '%s'", result.GPAIResult.Category)
|
||||
}
|
||||
if result.GPAIResult.IsSystemicRisk {
|
||||
t.Error("Expected IsSystemicRisk to be false")
|
||||
}
|
||||
|
||||
// Should have Art. 51, 53, 50 (generative)
|
||||
hasArt51 := false
|
||||
hasArt53 := false
|
||||
hasArt50 := false
|
||||
for _, a := range result.GPAIResult.ApplicableArticles {
|
||||
if a == "Art. 51" {
|
||||
hasArt51 = true
|
||||
}
|
||||
if a == "Art. 53" {
|
||||
hasArt53 = true
|
||||
}
|
||||
if a == "Art. 50" {
|
||||
hasArt50 = true
|
||||
}
|
||||
}
|
||||
if !hasArt51 {
|
||||
t.Error("Expected Art. 51 in applicable articles")
|
||||
}
|
||||
if !hasArt53 {
|
||||
t.Error("Expected Art. 53 in applicable articles")
|
||||
}
|
||||
if !hasArt50 {
|
||||
t.Error("Expected Art. 50 in applicable articles (generative AI)")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEvaluateDecisionTree_GPAI_SystemicRisk(t *testing.T) {
|
||||
// Q8=Yes, Q10=Yes → GPAI systemic risk
|
||||
req := &DecisionTreeEvalRequest{
|
||||
SystemName: "GPT-5",
|
||||
Answers: map[string]DecisionTreeAnswer{
|
||||
Q1: {QuestionID: Q1, Value: true},
|
||||
Q8: {QuestionID: Q8, Value: true},
|
||||
Q9: {QuestionID: Q9, Value: true},
|
||||
Q10: {QuestionID: Q10, Value: true},
|
||||
Q11: {QuestionID: Q11, Value: true},
|
||||
Q12: {QuestionID: Q12, Value: true},
|
||||
},
|
||||
}
|
||||
|
||||
result := EvaluateDecisionTree(req)
|
||||
|
||||
if !result.GPAIResult.IsGPAI {
|
||||
t.Error("Expected IsGPAI to be true")
|
||||
}
|
||||
if result.GPAIResult.Category != GPAICategorySystemic {
|
||||
t.Errorf("Expected category 'systemic', got '%s'", result.GPAIResult.Category)
|
||||
}
|
||||
if !result.GPAIResult.IsSystemicRisk {
|
||||
t.Error("Expected IsSystemicRisk to be true")
|
||||
}
|
||||
|
||||
// Should have Art. 55
|
||||
hasArt55 := false
|
||||
for _, a := range result.GPAIResult.ApplicableArticles {
|
||||
if a == "Art. 55" {
|
||||
hasArt55 = true
|
||||
}
|
||||
}
|
||||
if !hasArt55 {
|
||||
t.Error("Expected Art. 55 in applicable articles (systemic risk)")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEvaluateDecisionTree_Combined_HighRiskAndGPAI(t *testing.T) {
|
||||
// Q1=Yes, Q4=Yes (high risk) + Q8=Yes, Q9=Yes (GPAI standard)
|
||||
req := &DecisionTreeEvalRequest{
|
||||
SystemName: "HR Screening with LLM",
|
||||
SystemDescription: "LLM-based applicant screening system",
|
||||
Answers: map[string]DecisionTreeAnswer{
|
||||
Q1: {QuestionID: Q1, Value: true},
|
||||
Q2: {QuestionID: Q2, Value: false},
|
||||
Q3: {QuestionID: Q3, Value: false},
|
||||
Q4: {QuestionID: Q4, Value: true},
|
||||
Q5: {QuestionID: Q5, Value: false},
|
||||
Q6: {QuestionID: Q6, Value: false},
|
||||
Q7: {QuestionID: Q7, Value: true},
|
||||
Q8: {QuestionID: Q8, Value: true},
|
||||
Q9: {QuestionID: Q9, Value: true},
|
||||
Q10: {QuestionID: Q10, Value: false},
|
||||
Q11: {QuestionID: Q11, Value: false},
|
||||
Q12: {QuestionID: Q12, Value: false},
|
||||
},
|
||||
}
|
||||
|
||||
result := EvaluateDecisionTree(req)
|
||||
|
||||
// Both axes should be triggered
|
||||
if result.HighRiskResult != AIActHighRisk {
|
||||
t.Errorf("Expected high_risk, got %s", result.HighRiskResult)
|
||||
}
|
||||
if !result.GPAIResult.IsGPAI {
|
||||
t.Error("Expected GPAI to be true")
|
||||
}
|
||||
if result.GPAIResult.Category != GPAICategoryStandard {
|
||||
t.Errorf("Expected GPAI category 'standard', got '%s'", result.GPAIResult.Category)
|
||||
}
|
||||
|
||||
// Combined obligations should include both axes
|
||||
if len(result.CombinedObligations) < 5 {
|
||||
t.Errorf("Expected at least 5 combined obligations, got %d", len(result.CombinedObligations))
|
||||
}
|
||||
|
||||
// Should have articles from both axes
|
||||
if len(result.ApplicableArticles) < 3 {
|
||||
t.Errorf("Expected at least 3 applicable articles, got %d", len(result.ApplicableArticles))
|
||||
}
|
||||
|
||||
// Check system name preserved
|
||||
if result.SystemName != "HR Screening with LLM" {
|
||||
t.Errorf("Expected system name preserved, got '%s'", result.SystemName)
|
||||
}
|
||||
if result.SystemDescription != "LLM-based applicant screening system" {
|
||||
t.Errorf("Expected description preserved, got '%s'", result.SystemDescription)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEvaluateDecisionTree_GPAI_MarketPenetration(t *testing.T) {
|
||||
// Q8=Yes, Q10=No, Q12=Yes → GPAI standard with market penetration warning
|
||||
req := &DecisionTreeEvalRequest{
|
||||
SystemName: "Popular Chatbot",
|
||||
Answers: map[string]DecisionTreeAnswer{
|
||||
Q1: {QuestionID: Q1, Value: true},
|
||||
Q8: {QuestionID: Q8, Value: true},
|
||||
Q9: {QuestionID: Q9, Value: true},
|
||||
Q10: {QuestionID: Q10, Value: false},
|
||||
Q11: {QuestionID: Q11, Value: true},
|
||||
Q12: {QuestionID: Q12, Value: true},
|
||||
},
|
||||
}
|
||||
|
||||
result := EvaluateDecisionTree(req)
|
||||
|
||||
if result.GPAIResult.Category != GPAICategoryStandard {
|
||||
t.Errorf("Expected category 'standard' (not systemic because Q10=No), got '%s'", result.GPAIResult.Category)
|
||||
}
|
||||
|
||||
// Should have Art. 51 Abs. 3 warning
|
||||
hasArt51_3 := false
|
||||
for _, a := range result.GPAIResult.ApplicableArticles {
|
||||
if a == "Art. 51 Abs. 3" {
|
||||
hasArt51_3 = true
|
||||
}
|
||||
}
|
||||
if !hasArt51_3 {
|
||||
t.Error("Expected Art. 51 Abs. 3 in applicable articles for high market penetration")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEvaluateDecisionTree_NoGPAI(t *testing.T) {
|
||||
// Q8=No → No GPAI classification
|
||||
req := &DecisionTreeEvalRequest{
|
||||
SystemName: "Traditional ML",
|
||||
Answers: map[string]DecisionTreeAnswer{
|
||||
Q1: {QuestionID: Q1, Value: true},
|
||||
Q8: {QuestionID: Q8, Value: false},
|
||||
},
|
||||
}
|
||||
|
||||
result := EvaluateDecisionTree(req)
|
||||
|
||||
if result.GPAIResult.IsGPAI {
|
||||
t.Error("Expected IsGPAI to be false")
|
||||
}
|
||||
if result.GPAIResult.Category != GPAICategoryNone {
|
||||
t.Errorf("Expected category 'none', got '%s'", result.GPAIResult.Category)
|
||||
}
|
||||
if len(result.GPAIResult.Obligations) != 0 {
|
||||
t.Errorf("Expected 0 GPAI obligations, got %d", len(result.GPAIResult.Obligations))
|
||||
}
|
||||
}
|
||||
|
||||
func TestAnswerIsYes(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
answers map[string]DecisionTreeAnswer
|
||||
qID string
|
||||
expected bool
|
||||
}{
|
||||
{"yes answer", map[string]DecisionTreeAnswer{"Q1": {Value: true}}, "Q1", true},
|
||||
{"no answer", map[string]DecisionTreeAnswer{"Q1": {Value: false}}, "Q1", false},
|
||||
{"missing answer", map[string]DecisionTreeAnswer{}, "Q1", false},
|
||||
{"different question", map[string]DecisionTreeAnswer{"Q2": {Value: true}}, "Q1", false},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := answerIsYes(tt.answers, tt.qID)
|
||||
if result != tt.expected {
|
||||
t.Errorf("Expected %v, got %v", tt.expected, result)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
542
ai-compliance-sdk/internal/ucca/domain_context_test.go
Normal file
542
ai-compliance-sdk/internal/ucca/domain_context_test.go
Normal file
@@ -0,0 +1,542 @@
|
||||
package ucca
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// ============================================================================
|
||||
// HR Domain Context Tests
|
||||
// ============================================================================
|
||||
|
||||
func TestHRContext_AutomatedRejection_BLOCK(t *testing.T) {
|
||||
root := getProjectRoot(t)
|
||||
policyPath := filepath.Join(root, "policies", "ucca_policy_v1.yaml")
|
||||
engine, err := NewPolicyEngineFromPath(policyPath)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create policy engine: %v", err)
|
||||
}
|
||||
|
||||
intake := &UseCaseIntake{
|
||||
UseCaseText: "KI generiert und versendet Absagen automatisch",
|
||||
Domain: DomainHR,
|
||||
DataTypes: DataTypes{PersonalData: true, EmployeeData: true},
|
||||
HRContext: &HRContext{
|
||||
AutomatedScreening: true,
|
||||
AutomatedRejection: true,
|
||||
},
|
||||
}
|
||||
|
||||
result := engine.Evaluate(intake)
|
||||
|
||||
if result.Feasibility != FeasibilityNO {
|
||||
t.Errorf("Expected NO feasibility for automated rejection, got %s", result.Feasibility)
|
||||
}
|
||||
if !result.Art22Risk {
|
||||
t.Error("Expected Art22Risk=true for automated rejection")
|
||||
}
|
||||
}
|
||||
|
||||
func TestHRContext_ScreeningWithHumanReview_OK(t *testing.T) {
|
||||
root := getProjectRoot(t)
|
||||
policyPath := filepath.Join(root, "policies", "ucca_policy_v1.yaml")
|
||||
engine, err := NewPolicyEngineFromPath(policyPath)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create policy engine: %v", err)
|
||||
}
|
||||
|
||||
intake := &UseCaseIntake{
|
||||
UseCaseText: "KI sortiert Bewerber vor, Mensch prueft jeden Vorschlag",
|
||||
Domain: DomainHR,
|
||||
DataTypes: DataTypes{PersonalData: true, EmployeeData: true},
|
||||
HRContext: &HRContext{
|
||||
AutomatedScreening: true,
|
||||
AutomatedRejection: false,
|
||||
HumanReviewEnforced: true,
|
||||
BiasAuditsDone: true,
|
||||
},
|
||||
}
|
||||
|
||||
result := engine.Evaluate(intake)
|
||||
|
||||
// Should NOT block — human review is enforced
|
||||
if result.Feasibility == FeasibilityNO {
|
||||
t.Error("Expected feasibility != NO when human review is enforced")
|
||||
}
|
||||
}
|
||||
|
||||
func TestHRContext_AGGVisible_RiskIncrease(t *testing.T) {
|
||||
root := getProjectRoot(t)
|
||||
policyPath := filepath.Join(root, "policies", "ucca_policy_v1.yaml")
|
||||
engine, err := NewPolicyEngineFromPath(policyPath)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create policy engine: %v", err)
|
||||
}
|
||||
|
||||
intakeWithAGG := &UseCaseIntake{
|
||||
UseCaseText: "CV-Screening mit Foto und Name sichtbar",
|
||||
Domain: DomainHR,
|
||||
DataTypes: DataTypes{PersonalData: true, EmployeeData: true},
|
||||
HRContext: &HRContext{AGGCategoriesVisible: true},
|
||||
}
|
||||
intakeWithout := &UseCaseIntake{
|
||||
UseCaseText: "CV-Screening anonymisiert",
|
||||
Domain: DomainHR,
|
||||
DataTypes: DataTypes{PersonalData: true, EmployeeData: true},
|
||||
HRContext: &HRContext{AGGCategoriesVisible: false},
|
||||
}
|
||||
|
||||
resultWith := engine.Evaluate(intakeWithAGG)
|
||||
resultWithout := engine.Evaluate(intakeWithout)
|
||||
|
||||
if resultWith.RiskScore <= resultWithout.RiskScore {
|
||||
t.Errorf("Expected higher risk with AGG visible (%d) vs without (%d)",
|
||||
resultWith.RiskScore, resultWithout.RiskScore)
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Education Domain Context Tests
|
||||
// ============================================================================
|
||||
|
||||
func TestEducationContext_MinorsWithoutTeacher_BLOCK(t *testing.T) {
|
||||
root := getProjectRoot(t)
|
||||
policyPath := filepath.Join(root, "policies", "ucca_policy_v1.yaml")
|
||||
engine, err := NewPolicyEngineFromPath(policyPath)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create policy engine: %v", err)
|
||||
}
|
||||
|
||||
intake := &UseCaseIntake{
|
||||
UseCaseText: "KI bewertet Schuelerarbeiten ohne Lehrkraft-Pruefung",
|
||||
Domain: DomainEducation,
|
||||
DataTypes: DataTypes{PersonalData: true, MinorData: true},
|
||||
EducationContext: &EducationContext{
|
||||
GradeInfluence: true,
|
||||
MinorsInvolved: true,
|
||||
TeacherReviewRequired: false,
|
||||
},
|
||||
}
|
||||
|
||||
result := engine.Evaluate(intake)
|
||||
|
||||
if result.Feasibility != FeasibilityNO {
|
||||
t.Errorf("Expected NO feasibility for minors without teacher review, got %s", result.Feasibility)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEducationContext_WithTeacherReview_Allowed(t *testing.T) {
|
||||
root := getProjectRoot(t)
|
||||
policyPath := filepath.Join(root, "policies", "ucca_policy_v1.yaml")
|
||||
engine, err := NewPolicyEngineFromPath(policyPath)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create policy engine: %v", err)
|
||||
}
|
||||
|
||||
intake := &UseCaseIntake{
|
||||
UseCaseText: "KI schlaegt Noten vor, Lehrkraft prueft und entscheidet",
|
||||
Domain: DomainEducation,
|
||||
DataTypes: DataTypes{PersonalData: true, MinorData: true},
|
||||
EducationContext: &EducationContext{
|
||||
GradeInfluence: true,
|
||||
MinorsInvolved: true,
|
||||
TeacherReviewRequired: true,
|
||||
},
|
||||
}
|
||||
|
||||
result := engine.Evaluate(intake)
|
||||
|
||||
if result.Feasibility == FeasibilityNO {
|
||||
t.Error("Expected feasibility != NO when teacher review is required")
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Healthcare Domain Context Tests
|
||||
// ============================================================================
|
||||
|
||||
func TestHealthcareContext_MDRWithoutValidation_BLOCK(t *testing.T) {
|
||||
root := getProjectRoot(t)
|
||||
policyPath := filepath.Join(root, "policies", "ucca_policy_v1.yaml")
|
||||
engine, err := NewPolicyEngineFromPath(policyPath)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create policy engine: %v", err)
|
||||
}
|
||||
|
||||
intake := &UseCaseIntake{
|
||||
UseCaseText: "KI-Diagnosetool als Medizinprodukt ohne klinische Validierung",
|
||||
Domain: DomainHealthcare,
|
||||
DataTypes: DataTypes{PersonalData: true, Article9Data: true},
|
||||
HealthcareContext: &HealthcareContext{
|
||||
DiagnosisSupport: true,
|
||||
MedicalDevice: true,
|
||||
ClinicalValidation: false,
|
||||
},
|
||||
}
|
||||
|
||||
result := engine.Evaluate(intake)
|
||||
|
||||
if result.Feasibility != FeasibilityNO {
|
||||
t.Errorf("Expected NO for medical device without clinical validation, got %s", result.Feasibility)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHealthcareContext_Triage_HighRisk(t *testing.T) {
|
||||
root := getProjectRoot(t)
|
||||
policyPath := filepath.Join(root, "policies", "ucca_policy_v1.yaml")
|
||||
engine, err := NewPolicyEngineFromPath(policyPath)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create policy engine: %v", err)
|
||||
}
|
||||
|
||||
intake := &UseCaseIntake{
|
||||
UseCaseText: "KI priorisiert Patienten in der Notaufnahme",
|
||||
Domain: DomainHealthcare,
|
||||
DataTypes: DataTypes{PersonalData: true, Article9Data: true},
|
||||
HealthcareContext: &HealthcareContext{
|
||||
TriageDecision: true,
|
||||
PatientDataProcessed: true,
|
||||
},
|
||||
}
|
||||
|
||||
result := engine.Evaluate(intake)
|
||||
|
||||
if result.RiskScore < 40 {
|
||||
t.Errorf("Expected high risk score for triage, got %d", result.RiskScore)
|
||||
}
|
||||
if !result.DSFARecommended {
|
||||
t.Error("Expected DSFA recommended for triage")
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Critical Infrastructure Tests
|
||||
// ============================================================================
|
||||
|
||||
func TestCriticalInfra_SafetyCriticalNoRedundancy_BLOCK(t *testing.T) {
|
||||
root := getProjectRoot(t)
|
||||
policyPath := filepath.Join(root, "policies", "ucca_policy_v1.yaml")
|
||||
engine, err := NewPolicyEngineFromPath(policyPath)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create policy engine: %v", err)
|
||||
}
|
||||
|
||||
intake := &UseCaseIntake{
|
||||
UseCaseText: "KI steuert Stromnetz ohne Fallback",
|
||||
Domain: DomainEnergy,
|
||||
CriticalInfraContext: &CriticalInfraContext{
|
||||
GridControl: true,
|
||||
SafetyCritical: true,
|
||||
RedundancyExists: false,
|
||||
},
|
||||
}
|
||||
|
||||
result := engine.Evaluate(intake)
|
||||
|
||||
if result.Feasibility != FeasibilityNO {
|
||||
t.Errorf("Expected NO for safety-critical without redundancy, got %s", result.Feasibility)
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Marketing — Deepfake BLOCK Test
|
||||
// ============================================================================
|
||||
|
||||
func TestMarketing_DeepfakeUnlabeled_BLOCK(t *testing.T) {
|
||||
root := getProjectRoot(t)
|
||||
policyPath := filepath.Join(root, "policies", "ucca_policy_v1.yaml")
|
||||
engine, err := NewPolicyEngineFromPath(policyPath)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create policy engine: %v", err)
|
||||
}
|
||||
|
||||
intake := &UseCaseIntake{
|
||||
UseCaseText: "KI generiert Werbevideos ohne Kennzeichnung",
|
||||
Domain: DomainMarketing,
|
||||
MarketingContext: &MarketingContext{
|
||||
DeepfakeContent: true,
|
||||
AIContentLabeled: false,
|
||||
},
|
||||
}
|
||||
|
||||
result := engine.Evaluate(intake)
|
||||
|
||||
if result.Feasibility != FeasibilityNO {
|
||||
t.Errorf("Expected NO for unlabeled deepfakes, got %s", result.Feasibility)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMarketing_DeepfakeLabeled_OK(t *testing.T) {
|
||||
root := getProjectRoot(t)
|
||||
policyPath := filepath.Join(root, "policies", "ucca_policy_v1.yaml")
|
||||
engine, err := NewPolicyEngineFromPath(policyPath)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create policy engine: %v", err)
|
||||
}
|
||||
|
||||
intake := &UseCaseIntake{
|
||||
UseCaseText: "KI generiert Werbevideos mit Kennzeichnung",
|
||||
Domain: DomainMarketing,
|
||||
MarketingContext: &MarketingContext{
|
||||
DeepfakeContent: true,
|
||||
AIContentLabeled: true,
|
||||
},
|
||||
}
|
||||
|
||||
result := engine.Evaluate(intake)
|
||||
|
||||
if result.Feasibility == FeasibilityNO {
|
||||
t.Error("Expected feasibility != NO when deepfakes are properly labeled")
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Manufacturing — Safety BLOCK Test
|
||||
// ============================================================================
|
||||
|
||||
func TestManufacturing_SafetyUnvalidated_BLOCK(t *testing.T) {
|
||||
root := getProjectRoot(t)
|
||||
policyPath := filepath.Join(root, "policies", "ucca_policy_v1.yaml")
|
||||
engine, err := NewPolicyEngineFromPath(policyPath)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create policy engine: %v", err)
|
||||
}
|
||||
|
||||
intake := &UseCaseIntake{
|
||||
UseCaseText: "KI in Maschinensicherheit ohne Validierung",
|
||||
Domain: DomainMechanicalEngineering,
|
||||
ManufacturingContext: &ManufacturingContext{
|
||||
MachineSafety: true,
|
||||
SafetyValidated: false,
|
||||
},
|
||||
}
|
||||
|
||||
result := engine.Evaluate(intake)
|
||||
|
||||
if result.Feasibility != FeasibilityNO {
|
||||
t.Errorf("Expected NO for unvalidated machine safety, got %s", result.Feasibility)
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// AGG V2 Obligations Loading Test
|
||||
// ============================================================================
|
||||
|
||||
func TestAGGV2_LoadsFromManifest(t *testing.T) {
|
||||
regs, err := LoadAllV2Regulations()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to load v2 regulations: %v", err)
|
||||
}
|
||||
|
||||
agg, ok := regs["agg"]
|
||||
if !ok {
|
||||
t.Fatal("agg not found in loaded regulations")
|
||||
}
|
||||
|
||||
if len(agg.Obligations) < 8 {
|
||||
t.Errorf("Expected at least 8 AGG obligations, got %d", len(agg.Obligations))
|
||||
}
|
||||
|
||||
// Check first obligation
|
||||
if agg.Obligations[0].ID != "AGG-OBL-001" {
|
||||
t.Errorf("Expected first ID 'AGG-OBL-001', got '%s'", agg.Obligations[0].ID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAGGApplicability_Germany(t *testing.T) {
|
||||
regs, err := LoadAllV2Regulations()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to load v2 regulations: %v", err)
|
||||
}
|
||||
|
||||
module := NewJSONRegulationModule(regs["agg"])
|
||||
|
||||
factsDE := &UnifiedFacts{Organization: OrganizationFacts{Country: "DE"}}
|
||||
if !module.IsApplicable(factsDE) {
|
||||
t.Error("AGG should be applicable for German company")
|
||||
}
|
||||
|
||||
factsUS := &UnifiedFacts{Organization: OrganizationFacts{Country: "US"}}
|
||||
if module.IsApplicable(factsUS) {
|
||||
t.Error("AGG should NOT be applicable for US company")
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// AI Act V2 Extended Obligations Test
|
||||
// ============================================================================
|
||||
|
||||
func TestAIActV2_ExtendedObligations(t *testing.T) {
|
||||
regs, err := LoadAllV2Regulations()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to load v2 regulations: %v", err)
|
||||
}
|
||||
|
||||
aiAct, ok := regs["ai_act"]
|
||||
if !ok {
|
||||
t.Fatal("ai_act not found in loaded regulations")
|
||||
}
|
||||
|
||||
if len(aiAct.Obligations) < 75 {
|
||||
t.Errorf("Expected at least 75 AI Act obligations (expanded), got %d", len(aiAct.Obligations))
|
||||
}
|
||||
|
||||
// Check GPAI obligations exist (Art. 51-56)
|
||||
hasGPAI := false
|
||||
for _, obl := range aiAct.Obligations {
|
||||
if obl.ID == "AIACT-OBL-078" { // GPAI classification
|
||||
hasGPAI = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !hasGPAI {
|
||||
t.Error("Expected GPAI obligation AIACT-OBL-078 in expanded AI Act")
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Field Resolver Tests — Domain Contexts
|
||||
// ============================================================================
|
||||
|
||||
func TestFieldResolver_HRContext(t *testing.T) {
|
||||
root := getProjectRoot(t)
|
||||
policyPath := filepath.Join(root, "policies", "ucca_policy_v1.yaml")
|
||||
engine, err := NewPolicyEngineFromPath(policyPath)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create policy engine: %v", err)
|
||||
}
|
||||
|
||||
intake := &UseCaseIntake{
|
||||
HRContext: &HRContext{AutomatedScreening: true},
|
||||
}
|
||||
|
||||
val := engine.getFieldValue("hr_context.automated_screening", intake)
|
||||
if val != true {
|
||||
t.Errorf("Expected true for hr_context.automated_screening, got %v", val)
|
||||
}
|
||||
|
||||
val2 := engine.getFieldValue("hr_context.automated_rejection", intake)
|
||||
if val2 != false {
|
||||
t.Errorf("Expected false for hr_context.automated_rejection, got %v", val2)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFieldResolver_NilContext(t *testing.T) {
|
||||
root := getProjectRoot(t)
|
||||
policyPath := filepath.Join(root, "policies", "ucca_policy_v1.yaml")
|
||||
engine, err := NewPolicyEngineFromPath(policyPath)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create policy engine: %v", err)
|
||||
}
|
||||
|
||||
intake := &UseCaseIntake{} // No HR context
|
||||
|
||||
val := engine.getFieldValue("hr_context.automated_screening", intake)
|
||||
if val != nil {
|
||||
t.Errorf("Expected nil for nil HR context, got %v", val)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFieldResolver_HealthcareContext(t *testing.T) {
|
||||
root := getProjectRoot(t)
|
||||
policyPath := filepath.Join(root, "policies", "ucca_policy_v1.yaml")
|
||||
engine, err := NewPolicyEngineFromPath(policyPath)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create policy engine: %v", err)
|
||||
}
|
||||
|
||||
intake := &UseCaseIntake{
|
||||
HealthcareContext: &HealthcareContext{
|
||||
TriageDecision: true,
|
||||
MedicalDevice: false,
|
||||
},
|
||||
}
|
||||
|
||||
val := engine.getFieldValue("healthcare_context.triage_decision", intake)
|
||||
if val != true {
|
||||
t.Errorf("Expected true, got %v", val)
|
||||
}
|
||||
|
||||
val2 := engine.getFieldValue("healthcare_context.medical_device", intake)
|
||||
if val2 != false {
|
||||
t.Errorf("Expected false, got %v", val2)
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Hospitality — Review Manipulation BLOCK
|
||||
// ============================================================================
|
||||
|
||||
func TestHospitality_ReviewManipulation_BLOCK(t *testing.T) {
|
||||
root := getProjectRoot(t)
|
||||
policyPath := filepath.Join(root, "policies", "ucca_policy_v1.yaml")
|
||||
engine, err := NewPolicyEngineFromPath(policyPath)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create policy engine: %v", err)
|
||||
}
|
||||
|
||||
intake := &UseCaseIntake{
|
||||
UseCaseText: "KI generiert Fake-Bewertungen",
|
||||
Domain: DomainHospitality,
|
||||
HospitalityContext: &HospitalityContext{
|
||||
ReviewManipulation: true,
|
||||
},
|
||||
}
|
||||
|
||||
result := engine.Evaluate(intake)
|
||||
|
||||
if result.Feasibility != FeasibilityNO {
|
||||
t.Errorf("Expected NO for review manipulation, got %s", result.Feasibility)
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Total Obligations Count
|
||||
// ============================================================================
|
||||
|
||||
func TestTotalObligationsCount(t *testing.T) {
|
||||
regs, err := LoadAllV2Regulations()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to load v2 regulations: %v", err)
|
||||
}
|
||||
|
||||
total := 0
|
||||
for _, reg := range regs {
|
||||
total += len(reg.Obligations)
|
||||
}
|
||||
|
||||
// We expect at least 350 obligations across all regulations
|
||||
if total < 350 {
|
||||
t.Errorf("Expected at least 350 total obligations, got %d", total)
|
||||
}
|
||||
|
||||
t.Logf("Total obligations across all regulations: %d", total)
|
||||
for id, reg := range regs {
|
||||
t.Logf(" %s: %d obligations", id, len(reg.Obligations))
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Domain constant existence checks
|
||||
// ============================================================================
|
||||
|
||||
func TestDomainConstants_Exist(t *testing.T) {
|
||||
domains := []Domain{
|
||||
DomainHR, DomainEducation, DomainHealthcare,
|
||||
DomainFinance, DomainBanking, DomainInsurance,
|
||||
DomainEnergy, DomainUtilities,
|
||||
DomainAutomotive, DomainAerospace,
|
||||
DomainRetail, DomainEcommerce,
|
||||
DomainMarketing, DomainMedia,
|
||||
DomainLogistics, DomainConstruction,
|
||||
DomainPublicSector, DomainDefense,
|
||||
DomainMechanicalEngineering,
|
||||
}
|
||||
|
||||
for _, d := range domains {
|
||||
if d == "" {
|
||||
t.Error("Empty domain constant found")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package ucca
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
@@ -187,6 +188,12 @@ func (t *EscalationTrigger) DetermineEscalationLevel(result *AssessmentResult) (
|
||||
}
|
||||
}
|
||||
|
||||
// BetrVG E3: Very high conflict score without consultation
|
||||
if result.BetrvgConflictScore >= 75 && !result.Intake.WorksCouncilConsulted {
|
||||
reasons = append(reasons, "BetrVG-Konfliktpotenzial sehr hoch (Score "+fmt.Sprintf("%d", result.BetrvgConflictScore)+") ohne BR-Konsultation")
|
||||
return EscalationLevelE3, joinReasons(reasons, "E3 erforderlich: ")
|
||||
}
|
||||
|
||||
if hasArt9 || result.DSFARecommended || result.RiskScore > t.E2RiskThreshold {
|
||||
if result.DSFARecommended {
|
||||
reasons = append(reasons, "DSFA empfohlen")
|
||||
@@ -197,6 +204,12 @@ func (t *EscalationTrigger) DetermineEscalationLevel(result *AssessmentResult) (
|
||||
return EscalationLevelE2, joinReasons(reasons, "DSB-Konsultation erforderlich: ")
|
||||
}
|
||||
|
||||
// BetrVG E2: High conflict score
|
||||
if result.BetrvgConflictScore >= 50 && result.BetrvgConsultationRequired && !result.Intake.WorksCouncilConsulted {
|
||||
reasons = append(reasons, "BetrVG-Mitbestimmung erforderlich (Score "+fmt.Sprintf("%d", result.BetrvgConflictScore)+"), BR nicht konsultiert")
|
||||
return EscalationLevelE2, joinReasons(reasons, "BR-Konsultation erforderlich: ")
|
||||
}
|
||||
|
||||
// E1: Low priority checks
|
||||
// - WARN rules triggered
|
||||
// - Risk 20-40
|
||||
|
||||
@@ -56,6 +56,10 @@ func (m *JSONRegulationModule) defaultApplicability(facts *UnifiedFacts) bool {
|
||||
return facts.Organization.EUMember && facts.AIUsage.UsesAI
|
||||
case "dora":
|
||||
return facts.Financial.DORAApplies || facts.Financial.IsRegulated
|
||||
case "betrvg":
|
||||
return facts.Organization.Country == "DE" && facts.Organization.EmployeeCount >= 5
|
||||
case "agg":
|
||||
return facts.Organization.Country == "DE"
|
||||
default:
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -178,3 +178,73 @@ const (
|
||||
ExportFormatJSON ExportFormat = "json"
|
||||
ExportFormatMarkdown ExportFormat = "md"
|
||||
)
|
||||
|
||||
// ============================================================================
|
||||
// AI Act Decision Tree Types
|
||||
// ============================================================================
|
||||
|
||||
// GPAICategory represents the GPAI classification result
|
||||
type GPAICategory string
|
||||
|
||||
const (
|
||||
GPAICategoryNone GPAICategory = "none"
|
||||
GPAICategoryStandard GPAICategory = "standard"
|
||||
GPAICategorySystemic GPAICategory = "systemic"
|
||||
)
|
||||
|
||||
// GPAIClassification represents the result of the GPAI axis evaluation
|
||||
type GPAIClassification struct {
|
||||
IsGPAI bool `json:"is_gpai"`
|
||||
IsSystemicRisk bool `json:"is_systemic_risk"`
|
||||
Category GPAICategory `json:"gpai_category"`
|
||||
ApplicableArticles []string `json:"applicable_articles"`
|
||||
Obligations []string `json:"obligations"`
|
||||
}
|
||||
|
||||
// DecisionTreeAnswer represents a user's answer to a decision tree question
|
||||
type DecisionTreeAnswer struct {
|
||||
QuestionID string `json:"question_id"`
|
||||
Value bool `json:"value"`
|
||||
Note string `json:"note,omitempty"`
|
||||
}
|
||||
|
||||
// DecisionTreeQuestion represents a single question in the decision tree
|
||||
type DecisionTreeQuestion struct {
|
||||
ID string `json:"id"`
|
||||
Axis string `json:"axis"` // "high_risk" or "gpai"
|
||||
Question string `json:"question"`
|
||||
Description string `json:"description"` // Additional context
|
||||
ArticleRef string `json:"article_ref"` // e.g., "Art. 5", "Anhang III"
|
||||
SkipIf string `json:"skip_if,omitempty"` // Question ID — skip if that was answered "no"
|
||||
}
|
||||
|
||||
// DecisionTreeDefinition represents the full decision tree structure for the frontend
|
||||
type DecisionTreeDefinition struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Version string `json:"version"`
|
||||
Questions []DecisionTreeQuestion `json:"questions"`
|
||||
}
|
||||
|
||||
// DecisionTreeEvalRequest is the API request for evaluating the decision tree
|
||||
type DecisionTreeEvalRequest struct {
|
||||
SystemName string `json:"system_name"`
|
||||
SystemDescription string `json:"system_description,omitempty"`
|
||||
Answers map[string]DecisionTreeAnswer `json:"answers"`
|
||||
}
|
||||
|
||||
// DecisionTreeResult represents the combined evaluation result
|
||||
type DecisionTreeResult struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
TenantID uuid.UUID `json:"tenant_id"`
|
||||
ProjectID *uuid.UUID `json:"project_id,omitempty"`
|
||||
SystemName string `json:"system_name"`
|
||||
SystemDescription string `json:"system_description,omitempty"`
|
||||
Answers map[string]DecisionTreeAnswer `json:"answers"`
|
||||
HighRiskResult AIActRiskLevel `json:"high_risk_result"`
|
||||
GPAIResult GPAIClassification `json:"gpai_result"`
|
||||
CombinedObligations []string `json:"combined_obligations"`
|
||||
ApplicableArticles []string `json:"applicable_articles"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
274
ai-compliance-sdk/internal/ucca/registration_store.go
Normal file
274
ai-compliance-sdk/internal/ucca/registration_store.go
Normal file
@@ -0,0 +1,274 @@
|
||||
package ucca
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
)
|
||||
|
||||
// AIRegistration represents an EU AI Database registration entry
|
||||
type AIRegistration struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
TenantID uuid.UUID `json:"tenant_id"`
|
||||
|
||||
// System
|
||||
SystemName string `json:"system_name"`
|
||||
SystemVersion string `json:"system_version,omitempty"`
|
||||
SystemDescription string `json:"system_description,omitempty"`
|
||||
IntendedPurpose string `json:"intended_purpose,omitempty"`
|
||||
|
||||
// Provider
|
||||
ProviderName string `json:"provider_name,omitempty"`
|
||||
ProviderLegalForm string `json:"provider_legal_form,omitempty"`
|
||||
ProviderAddress string `json:"provider_address,omitempty"`
|
||||
ProviderCountry string `json:"provider_country,omitempty"`
|
||||
EURepresentativeName string `json:"eu_representative_name,omitempty"`
|
||||
EURepresentativeContact string `json:"eu_representative_contact,omitempty"`
|
||||
|
||||
// Classification
|
||||
RiskClassification string `json:"risk_classification"`
|
||||
AnnexIIICategory string `json:"annex_iii_category,omitempty"`
|
||||
GPAIClassification string `json:"gpai_classification"`
|
||||
|
||||
// Conformity
|
||||
ConformityAssessmentType string `json:"conformity_assessment_type,omitempty"`
|
||||
NotifiedBodyName string `json:"notified_body_name,omitempty"`
|
||||
NotifiedBodyID string `json:"notified_body_id,omitempty"`
|
||||
CEMarking bool `json:"ce_marking"`
|
||||
|
||||
// Training data
|
||||
TrainingDataCategories json.RawMessage `json:"training_data_categories,omitempty"`
|
||||
TrainingDataSummary string `json:"training_data_summary,omitempty"`
|
||||
|
||||
// Status
|
||||
RegistrationStatus string `json:"registration_status"`
|
||||
EUDatabaseID string `json:"eu_database_id,omitempty"`
|
||||
RegistrationDate *time.Time `json:"registration_date,omitempty"`
|
||||
LastUpdateDate *time.Time `json:"last_update_date,omitempty"`
|
||||
|
||||
// Links
|
||||
UCCAAssessmentID *uuid.UUID `json:"ucca_assessment_id,omitempty"`
|
||||
DecisionTreeResultID *uuid.UUID `json:"decision_tree_result_id,omitempty"`
|
||||
|
||||
// Export
|
||||
ExportData json.RawMessage `json:"export_data,omitempty"`
|
||||
|
||||
// Audit
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
CreatedBy string `json:"created_by,omitempty"`
|
||||
SubmittedBy string `json:"submitted_by,omitempty"`
|
||||
}
|
||||
|
||||
// RegistrationStore handles AI registration persistence
|
||||
type RegistrationStore struct {
|
||||
pool *pgxpool.Pool
|
||||
}
|
||||
|
||||
// NewRegistrationStore creates a new registration store
|
||||
func NewRegistrationStore(pool *pgxpool.Pool) *RegistrationStore {
|
||||
return &RegistrationStore{pool: pool}
|
||||
}
|
||||
|
||||
// Create creates a new registration
|
||||
func (s *RegistrationStore) Create(ctx context.Context, r *AIRegistration) error {
|
||||
r.ID = uuid.New()
|
||||
r.CreatedAt = time.Now()
|
||||
r.UpdatedAt = time.Now()
|
||||
if r.RegistrationStatus == "" {
|
||||
r.RegistrationStatus = "draft"
|
||||
}
|
||||
if r.RiskClassification == "" {
|
||||
r.RiskClassification = "not_classified"
|
||||
}
|
||||
if r.GPAIClassification == "" {
|
||||
r.GPAIClassification = "none"
|
||||
}
|
||||
|
||||
_, err := s.pool.Exec(ctx, `
|
||||
INSERT INTO ai_system_registrations (
|
||||
id, tenant_id, system_name, system_version, system_description, intended_purpose,
|
||||
provider_name, provider_legal_form, provider_address, provider_country,
|
||||
eu_representative_name, eu_representative_contact,
|
||||
risk_classification, annex_iii_category, gpai_classification,
|
||||
conformity_assessment_type, notified_body_name, notified_body_id, ce_marking,
|
||||
training_data_categories, training_data_summary,
|
||||
registration_status, ucca_assessment_id, decision_tree_result_id,
|
||||
created_by
|
||||
) VALUES (
|
||||
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12,
|
||||
$13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25
|
||||
)`,
|
||||
r.ID, r.TenantID, r.SystemName, r.SystemVersion, r.SystemDescription, r.IntendedPurpose,
|
||||
r.ProviderName, r.ProviderLegalForm, r.ProviderAddress, r.ProviderCountry,
|
||||
r.EURepresentativeName, r.EURepresentativeContact,
|
||||
r.RiskClassification, r.AnnexIIICategory, r.GPAIClassification,
|
||||
r.ConformityAssessmentType, r.NotifiedBodyName, r.NotifiedBodyID, r.CEMarking,
|
||||
r.TrainingDataCategories, r.TrainingDataSummary,
|
||||
r.RegistrationStatus, r.UCCAAssessmentID, r.DecisionTreeResultID,
|
||||
r.CreatedBy,
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
// List returns all registrations for a tenant
|
||||
func (s *RegistrationStore) List(ctx context.Context, tenantID uuid.UUID) ([]AIRegistration, error) {
|
||||
rows, err := s.pool.Query(ctx, `
|
||||
SELECT id, tenant_id, system_name, system_version, system_description, intended_purpose,
|
||||
provider_name, provider_legal_form, provider_address, provider_country,
|
||||
eu_representative_name, eu_representative_contact,
|
||||
risk_classification, annex_iii_category, gpai_classification,
|
||||
conformity_assessment_type, notified_body_name, notified_body_id, ce_marking,
|
||||
training_data_categories, training_data_summary,
|
||||
registration_status, eu_database_id, registration_date, last_update_date,
|
||||
ucca_assessment_id, decision_tree_result_id, export_data,
|
||||
created_at, updated_at, created_by, submitted_by
|
||||
FROM ai_system_registrations
|
||||
WHERE tenant_id = $1
|
||||
ORDER BY created_at DESC`,
|
||||
tenantID,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var registrations []AIRegistration
|
||||
for rows.Next() {
|
||||
var r AIRegistration
|
||||
err := rows.Scan(
|
||||
&r.ID, &r.TenantID, &r.SystemName, &r.SystemVersion, &r.SystemDescription, &r.IntendedPurpose,
|
||||
&r.ProviderName, &r.ProviderLegalForm, &r.ProviderAddress, &r.ProviderCountry,
|
||||
&r.EURepresentativeName, &r.EURepresentativeContact,
|
||||
&r.RiskClassification, &r.AnnexIIICategory, &r.GPAIClassification,
|
||||
&r.ConformityAssessmentType, &r.NotifiedBodyName, &r.NotifiedBodyID, &r.CEMarking,
|
||||
&r.TrainingDataCategories, &r.TrainingDataSummary,
|
||||
&r.RegistrationStatus, &r.EUDatabaseID, &r.RegistrationDate, &r.LastUpdateDate,
|
||||
&r.UCCAAssessmentID, &r.DecisionTreeResultID, &r.ExportData,
|
||||
&r.CreatedAt, &r.UpdatedAt, &r.CreatedBy, &r.SubmittedBy,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
registrations = append(registrations, r)
|
||||
}
|
||||
return registrations, nil
|
||||
}
|
||||
|
||||
// GetByID returns a registration by ID
|
||||
func (s *RegistrationStore) GetByID(ctx context.Context, id uuid.UUID) (*AIRegistration, error) {
|
||||
var r AIRegistration
|
||||
err := s.pool.QueryRow(ctx, `
|
||||
SELECT id, tenant_id, system_name, system_version, system_description, intended_purpose,
|
||||
provider_name, provider_legal_form, provider_address, provider_country,
|
||||
eu_representative_name, eu_representative_contact,
|
||||
risk_classification, annex_iii_category, gpai_classification,
|
||||
conformity_assessment_type, notified_body_name, notified_body_id, ce_marking,
|
||||
training_data_categories, training_data_summary,
|
||||
registration_status, eu_database_id, registration_date, last_update_date,
|
||||
ucca_assessment_id, decision_tree_result_id, export_data,
|
||||
created_at, updated_at, created_by, submitted_by
|
||||
FROM ai_system_registrations
|
||||
WHERE id = $1`,
|
||||
id,
|
||||
).Scan(
|
||||
&r.ID, &r.TenantID, &r.SystemName, &r.SystemVersion, &r.SystemDescription, &r.IntendedPurpose,
|
||||
&r.ProviderName, &r.ProviderLegalForm, &r.ProviderAddress, &r.ProviderCountry,
|
||||
&r.EURepresentativeName, &r.EURepresentativeContact,
|
||||
&r.RiskClassification, &r.AnnexIIICategory, &r.GPAIClassification,
|
||||
&r.ConformityAssessmentType, &r.NotifiedBodyName, &r.NotifiedBodyID, &r.CEMarking,
|
||||
&r.TrainingDataCategories, &r.TrainingDataSummary,
|
||||
&r.RegistrationStatus, &r.EUDatabaseID, &r.RegistrationDate, &r.LastUpdateDate,
|
||||
&r.UCCAAssessmentID, &r.DecisionTreeResultID, &r.ExportData,
|
||||
&r.CreatedAt, &r.UpdatedAt, &r.CreatedBy, &r.SubmittedBy,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &r, nil
|
||||
}
|
||||
|
||||
// Update updates a registration
|
||||
func (s *RegistrationStore) Update(ctx context.Context, r *AIRegistration) error {
|
||||
r.UpdatedAt = time.Now()
|
||||
_, err := s.pool.Exec(ctx, `
|
||||
UPDATE ai_system_registrations SET
|
||||
system_name = $2, system_version = $3, system_description = $4, intended_purpose = $5,
|
||||
provider_name = $6, provider_legal_form = $7, provider_address = $8, provider_country = $9,
|
||||
eu_representative_name = $10, eu_representative_contact = $11,
|
||||
risk_classification = $12, annex_iii_category = $13, gpai_classification = $14,
|
||||
conformity_assessment_type = $15, notified_body_name = $16, notified_body_id = $17, ce_marking = $18,
|
||||
training_data_categories = $19, training_data_summary = $20,
|
||||
registration_status = $21, eu_database_id = $22,
|
||||
export_data = $23, updated_at = $24, submitted_by = $25
|
||||
WHERE id = $1`,
|
||||
r.ID, r.SystemName, r.SystemVersion, r.SystemDescription, r.IntendedPurpose,
|
||||
r.ProviderName, r.ProviderLegalForm, r.ProviderAddress, r.ProviderCountry,
|
||||
r.EURepresentativeName, r.EURepresentativeContact,
|
||||
r.RiskClassification, r.AnnexIIICategory, r.GPAIClassification,
|
||||
r.ConformityAssessmentType, r.NotifiedBodyName, r.NotifiedBodyID, r.CEMarking,
|
||||
r.TrainingDataCategories, r.TrainingDataSummary,
|
||||
r.RegistrationStatus, r.EUDatabaseID,
|
||||
r.ExportData, r.UpdatedAt, r.SubmittedBy,
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
// UpdateStatus changes only the registration status
|
||||
func (s *RegistrationStore) UpdateStatus(ctx context.Context, id uuid.UUID, status string, submittedBy string) error {
|
||||
now := time.Now()
|
||||
_, err := s.pool.Exec(ctx, `
|
||||
UPDATE ai_system_registrations
|
||||
SET registration_status = $2, submitted_by = $3, updated_at = $4,
|
||||
registration_date = CASE WHEN $2 = 'submitted' THEN $4 ELSE registration_date END,
|
||||
last_update_date = $4
|
||||
WHERE id = $1`,
|
||||
id, status, submittedBy, now,
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
// BuildExportJSON creates the EU AI Database submission JSON
|
||||
func (s *RegistrationStore) BuildExportJSON(r *AIRegistration) json.RawMessage {
|
||||
export := map[string]interface{}{
|
||||
"schema_version": "1.0",
|
||||
"submission_type": "ai_system_registration",
|
||||
"regulation": "EU AI Act (EU) 2024/1689",
|
||||
"article": "Art. 49",
|
||||
"provider": map[string]interface{}{
|
||||
"name": r.ProviderName,
|
||||
"legal_form": r.ProviderLegalForm,
|
||||
"address": r.ProviderAddress,
|
||||
"country": r.ProviderCountry,
|
||||
"eu_representative": r.EURepresentativeName,
|
||||
"eu_rep_contact": r.EURepresentativeContact,
|
||||
},
|
||||
"system": map[string]interface{}{
|
||||
"name": r.SystemName,
|
||||
"version": r.SystemVersion,
|
||||
"description": r.SystemDescription,
|
||||
"purpose": r.IntendedPurpose,
|
||||
},
|
||||
"classification": map[string]interface{}{
|
||||
"risk_level": r.RiskClassification,
|
||||
"annex_iii_category": r.AnnexIIICategory,
|
||||
"gpai": r.GPAIClassification,
|
||||
},
|
||||
"conformity": map[string]interface{}{
|
||||
"assessment_type": r.ConformityAssessmentType,
|
||||
"notified_body": r.NotifiedBodyName,
|
||||
"notified_body_id": r.NotifiedBodyID,
|
||||
"ce_marking": r.CEMarking,
|
||||
},
|
||||
"training_data": map[string]interface{}{
|
||||
"categories": r.TrainingDataCategories,
|
||||
"summary": r.TrainingDataSummary,
|
||||
},
|
||||
"status": r.RegistrationStatus,
|
||||
}
|
||||
data, _ := json.Marshal(export)
|
||||
return data
|
||||
}
|
||||
@@ -358,6 +358,128 @@ type AssessmentFilters struct {
|
||||
Offset int // OFFSET for pagination
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Decision Tree Result CRUD
|
||||
// ============================================================================
|
||||
|
||||
// CreateDecisionTreeResult stores a new decision tree result
|
||||
func (s *Store) CreateDecisionTreeResult(ctx context.Context, r *DecisionTreeResult) error {
|
||||
r.ID = uuid.New()
|
||||
r.CreatedAt = time.Now().UTC()
|
||||
r.UpdatedAt = r.CreatedAt
|
||||
|
||||
answers, _ := json.Marshal(r.Answers)
|
||||
gpaiResult, _ := json.Marshal(r.GPAIResult)
|
||||
obligations, _ := json.Marshal(r.CombinedObligations)
|
||||
articles, _ := json.Marshal(r.ApplicableArticles)
|
||||
|
||||
_, err := s.pool.Exec(ctx, `
|
||||
INSERT INTO ai_act_decision_tree_results (
|
||||
id, tenant_id, project_id, system_name, system_description,
|
||||
answers, high_risk_level, gpai_result,
|
||||
combined_obligations, applicable_articles,
|
||||
created_at, updated_at
|
||||
) VALUES (
|
||||
$1, $2, $3, $4, $5,
|
||||
$6, $7, $8,
|
||||
$9, $10,
|
||||
$11, $12
|
||||
)
|
||||
`,
|
||||
r.ID, r.TenantID, r.ProjectID, r.SystemName, r.SystemDescription,
|
||||
answers, string(r.HighRiskResult), gpaiResult,
|
||||
obligations, articles,
|
||||
r.CreatedAt, r.UpdatedAt,
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
// GetDecisionTreeResult retrieves a decision tree result by ID
|
||||
func (s *Store) GetDecisionTreeResult(ctx context.Context, id uuid.UUID) (*DecisionTreeResult, error) {
|
||||
var r DecisionTreeResult
|
||||
var answersBytes, gpaiBytes, oblBytes, artBytes []byte
|
||||
var highRiskLevel string
|
||||
|
||||
err := s.pool.QueryRow(ctx, `
|
||||
SELECT id, tenant_id, project_id, system_name, system_description,
|
||||
answers, high_risk_level, gpai_result,
|
||||
combined_obligations, applicable_articles,
|
||||
created_at, updated_at
|
||||
FROM ai_act_decision_tree_results WHERE id = $1
|
||||
`, id).Scan(
|
||||
&r.ID, &r.TenantID, &r.ProjectID, &r.SystemName, &r.SystemDescription,
|
||||
&answersBytes, &highRiskLevel, &gpaiBytes,
|
||||
&oblBytes, &artBytes,
|
||||
&r.CreatedAt, &r.UpdatedAt,
|
||||
)
|
||||
|
||||
if err == pgx.ErrNoRows {
|
||||
return nil, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
json.Unmarshal(answersBytes, &r.Answers)
|
||||
json.Unmarshal(gpaiBytes, &r.GPAIResult)
|
||||
json.Unmarshal(oblBytes, &r.CombinedObligations)
|
||||
json.Unmarshal(artBytes, &r.ApplicableArticles)
|
||||
r.HighRiskResult = AIActRiskLevel(highRiskLevel)
|
||||
|
||||
return &r, nil
|
||||
}
|
||||
|
||||
// ListDecisionTreeResults lists all decision tree results for a tenant
|
||||
func (s *Store) ListDecisionTreeResults(ctx context.Context, tenantID uuid.UUID) ([]DecisionTreeResult, error) {
|
||||
rows, err := s.pool.Query(ctx, `
|
||||
SELECT id, tenant_id, project_id, system_name, system_description,
|
||||
answers, high_risk_level, gpai_result,
|
||||
combined_obligations, applicable_articles,
|
||||
created_at, updated_at
|
||||
FROM ai_act_decision_tree_results
|
||||
WHERE tenant_id = $1
|
||||
ORDER BY created_at DESC
|
||||
LIMIT 100
|
||||
`, tenantID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var results []DecisionTreeResult
|
||||
for rows.Next() {
|
||||
var r DecisionTreeResult
|
||||
var answersBytes, gpaiBytes, oblBytes, artBytes []byte
|
||||
var highRiskLevel string
|
||||
|
||||
err := rows.Scan(
|
||||
&r.ID, &r.TenantID, &r.ProjectID, &r.SystemName, &r.SystemDescription,
|
||||
&answersBytes, &highRiskLevel, &gpaiBytes,
|
||||
&oblBytes, &artBytes,
|
||||
&r.CreatedAt, &r.UpdatedAt,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
json.Unmarshal(answersBytes, &r.Answers)
|
||||
json.Unmarshal(gpaiBytes, &r.GPAIResult)
|
||||
json.Unmarshal(oblBytes, &r.CombinedObligations)
|
||||
json.Unmarshal(artBytes, &r.ApplicableArticles)
|
||||
r.HighRiskResult = AIActRiskLevel(highRiskLevel)
|
||||
|
||||
results = append(results, r)
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
// DeleteDecisionTreeResult deletes a decision tree result by ID
|
||||
func (s *Store) DeleteDecisionTreeResult(ctx context.Context, id uuid.UUID) error {
|
||||
_, err := s.pool.Exec(ctx, "DELETE FROM ai_act_decision_tree_results WHERE id = $1", id)
|
||||
return err
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Helpers
|
||||
// ============================================================================
|
||||
|
||||
65
ai-compliance-sdk/migrations/023_ai_registration_schema.sql
Normal file
65
ai-compliance-sdk/migrations/023_ai_registration_schema.sql
Normal file
@@ -0,0 +1,65 @@
|
||||
-- Migration 023: AI System Registration Schema (Art. 49 AI Act)
|
||||
-- Tracks EU AI Database registrations for High-Risk AI systems
|
||||
|
||||
CREATE TABLE IF NOT EXISTS ai_system_registrations (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
tenant_id UUID NOT NULL,
|
||||
|
||||
-- System identification
|
||||
system_name VARCHAR(500) NOT NULL,
|
||||
system_version VARCHAR(100),
|
||||
system_description TEXT,
|
||||
intended_purpose TEXT,
|
||||
|
||||
-- Provider info
|
||||
provider_name VARCHAR(500),
|
||||
provider_legal_form VARCHAR(200),
|
||||
provider_address TEXT,
|
||||
provider_country VARCHAR(10),
|
||||
eu_representative_name VARCHAR(500),
|
||||
eu_representative_contact TEXT,
|
||||
|
||||
-- Classification
|
||||
risk_classification VARCHAR(50) DEFAULT 'not_classified',
|
||||
-- CHECK (risk_classification IN ('not_classified', 'minimal_risk', 'limited_risk', 'high_risk', 'unacceptable'))
|
||||
annex_iii_category VARCHAR(200),
|
||||
gpai_classification VARCHAR(50) DEFAULT 'none',
|
||||
-- CHECK (gpai_classification IN ('none', 'standard', 'systemic'))
|
||||
|
||||
-- Conformity
|
||||
conformity_assessment_type VARCHAR(50),
|
||||
-- CHECK (conformity_assessment_type IN ('internal', 'third_party', 'not_required'))
|
||||
notified_body_name VARCHAR(500),
|
||||
notified_body_id VARCHAR(100),
|
||||
ce_marking BOOLEAN DEFAULT false,
|
||||
|
||||
-- Training data
|
||||
training_data_categories JSONB DEFAULT '[]'::jsonb,
|
||||
training_data_summary TEXT,
|
||||
|
||||
-- Registration status
|
||||
registration_status VARCHAR(50) DEFAULT 'draft',
|
||||
-- CHECK (registration_status IN ('draft', 'ready', 'submitted', 'registered', 'update_required', 'withdrawn'))
|
||||
eu_database_id VARCHAR(200),
|
||||
registration_date TIMESTAMPTZ,
|
||||
last_update_date TIMESTAMPTZ,
|
||||
|
||||
-- Links to other assessments
|
||||
ucca_assessment_id UUID,
|
||||
decision_tree_result_id UUID,
|
||||
|
||||
-- Export data (cached JSON for EU submission)
|
||||
export_data JSONB,
|
||||
|
||||
-- Audit
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
created_by VARCHAR(200),
|
||||
submitted_by VARCHAR(200)
|
||||
);
|
||||
|
||||
-- Indexes
|
||||
CREATE INDEX IF NOT EXISTS idx_air_tenant ON ai_system_registrations (tenant_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_air_status ON ai_system_registrations (registration_status);
|
||||
CREATE INDEX IF NOT EXISTS idx_air_classification ON ai_system_registrations (risk_classification);
|
||||
CREATE INDEX IF NOT EXISTS idx_air_ucca ON ai_system_registrations (ucca_assessment_id);
|
||||
@@ -0,0 +1,45 @@
|
||||
-- Migration 024: Payment Compliance Schema
|
||||
-- Tracks payment terminal compliance assessments against control library
|
||||
|
||||
CREATE TABLE IF NOT EXISTS payment_compliance_assessments (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
tenant_id UUID NOT NULL,
|
||||
|
||||
-- Project / Tender
|
||||
project_name VARCHAR(500) NOT NULL,
|
||||
tender_reference VARCHAR(200),
|
||||
customer_name VARCHAR(500),
|
||||
description TEXT,
|
||||
|
||||
-- Scope
|
||||
system_type VARCHAR(100), -- terminal, backend, both, full_stack
|
||||
payment_methods JSONB DEFAULT '[]'::jsonb, -- ["card", "nfc", "girocard", "credit"]
|
||||
protocols JSONB DEFAULT '[]'::jsonb, -- ["zvt", "opi", "emv"]
|
||||
|
||||
-- Assessment
|
||||
total_controls INT DEFAULT 0,
|
||||
controls_passed INT DEFAULT 0,
|
||||
controls_failed INT DEFAULT 0,
|
||||
controls_partial INT DEFAULT 0,
|
||||
controls_not_applicable INT DEFAULT 0,
|
||||
controls_not_checked INT DEFAULT 0,
|
||||
compliance_score NUMERIC(5,2) DEFAULT 0,
|
||||
|
||||
-- Status
|
||||
status VARCHAR(50) DEFAULT 'draft',
|
||||
-- CHECK (status IN ('draft', 'in_progress', 'completed', 'approved'))
|
||||
|
||||
-- Results (per control)
|
||||
control_results JSONB DEFAULT '[]'::jsonb,
|
||||
-- Each entry: {"control_id": "PAY-001", "verdict": "passed|failed|partial|na|unchecked", "evidence": "...", "notes": "..."}
|
||||
|
||||
-- Audit
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
created_by VARCHAR(200),
|
||||
approved_by VARCHAR(200),
|
||||
approved_at TIMESTAMPTZ
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_pca_tenant ON payment_compliance_assessments (tenant_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_pca_status ON payment_compliance_assessments (status);
|
||||
37
ai-compliance-sdk/migrations/025_tender_analysis_schema.sql
Normal file
37
ai-compliance-sdk/migrations/025_tender_analysis_schema.sql
Normal file
@@ -0,0 +1,37 @@
|
||||
-- Migration 025: Tender Analysis Schema
|
||||
-- Stores uploaded tender documents, extracted requirements, and control matching results
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tender_analyses (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
tenant_id UUID NOT NULL,
|
||||
|
||||
-- Document
|
||||
file_name VARCHAR(500) NOT NULL,
|
||||
file_size BIGINT DEFAULT 0,
|
||||
file_content BYTEA,
|
||||
|
||||
-- Project
|
||||
project_name VARCHAR(500),
|
||||
customer_name VARCHAR(500),
|
||||
|
||||
-- Status
|
||||
status VARCHAR(50) DEFAULT 'uploaded',
|
||||
-- CHECK (status IN ('uploaded', 'extracting', 'extracted', 'matched', 'completed', 'error'))
|
||||
|
||||
-- Extracted requirements
|
||||
requirements JSONB DEFAULT '[]'::jsonb,
|
||||
total_requirements INT DEFAULT 0,
|
||||
|
||||
-- Match results
|
||||
match_results JSONB DEFAULT '[]'::jsonb,
|
||||
matched_count INT DEFAULT 0,
|
||||
unmatched_count INT DEFAULT 0,
|
||||
partial_count INT DEFAULT 0,
|
||||
|
||||
-- Audit
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_ta_tenant ON tender_analyses (tenant_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_ta_status ON tender_analyses (status);
|
||||
65
ai-compliance-sdk/payment-compliance-pack/README.md
Normal file
65
ai-compliance-sdk/payment-compliance-pack/README.md
Normal file
@@ -0,0 +1,65 @@
|
||||
# Payment Compliance Pack
|
||||
|
||||
Ausfuehrbares Pruefpaket fuer Payment-Terminal-Systeme.
|
||||
|
||||
## Inhalt
|
||||
|
||||
### Semgrep-Regeln (25 Regeln)
|
||||
|
||||
| Datei | Regeln | Controls |
|
||||
|-------|--------|----------|
|
||||
| `payment_logging.yml` | 5 | LOG-001, LOG-002, LOG-014 |
|
||||
| `payment_crypto.yml` | 6 | CRYPTO-001, CRYPTO-008, CRYPTO-009, KEYMGMT-001 |
|
||||
| `payment_api.yml` | 5 | API-004, API-005, API-014, API-017 |
|
||||
| `payment_config.yml` | 5 | CONFIG-001 bis CONFIG-004 |
|
||||
| `payment_data.yml` | 5 | DATA-004, DATA-005, DATA-013, TELEMETRY-001 |
|
||||
|
||||
### CodeQL-Specs (5 Queries)
|
||||
|
||||
| Datei | Ziel | Controls |
|
||||
|-------|------|----------|
|
||||
| `sensitive-data-to-logs.md` | Datenfluss zu Loggern | LOG-001, LOG-002, DATA-013 |
|
||||
| `sensitive-data-to-response.md` | Datenfluss in HTTP-Responses | API-009, ERROR-005 |
|
||||
| `tenant-context-loss.md` | Mandantenkontext-Verlust | TENANT-001, TENANT-002 |
|
||||
| `sensitive-data-to-telemetry.md` | Datenfluss in Telemetrie | TELEMETRY-001, TELEMETRY-002 |
|
||||
| `cache-export-leak.md` | Leaks in Cache/Export | DATA-004, DATA-011 |
|
||||
|
||||
### State-Machine-Tests (10 Testfaelle)
|
||||
|
||||
| Datei | Inhalt |
|
||||
|-------|--------|
|
||||
| `terminal_states.md` | 11 Zustaende, 15 Events, Transitions |
|
||||
| `terminal_invariants.md` | 8 Invarianten |
|
||||
| `terminal_testcases.json` | 10 ausfuehrbare Testfaelle |
|
||||
|
||||
### Finding-Schema
|
||||
|
||||
| Datei | Beschreibung |
|
||||
|-------|-------------|
|
||||
| `finding.schema.json` | JSON Schema fuer Pruefergebnisse |
|
||||
|
||||
## Ausfuehrung
|
||||
|
||||
### Semgrep
|
||||
|
||||
```bash
|
||||
semgrep --config payment-compliance-pack/semgrep/ /path/to/source
|
||||
```
|
||||
|
||||
### State-Machine-Tests
|
||||
|
||||
Die Testfaelle in `terminal_testcases.json` definieren:
|
||||
- Ausgangszustand
|
||||
- Event-Sequenz
|
||||
- Erwarteten Endzustand
|
||||
- Zu pruefende Invarianten
|
||||
- Gemappte Controls
|
||||
|
||||
Diese koennen gegen einen Terminal-Adapter oder Simulator ausgefuehrt werden.
|
||||
|
||||
## Priorisierte Umsetzung
|
||||
|
||||
1. **Welle 1:** 25 Semgrep-Regeln sofort produktiv
|
||||
2. **Welle 2:** 5 CodeQL-Queries fuer Datenfluesse
|
||||
3. **Welle 3:** 10 State-Machine-Tests gegen Terminal-Simulator
|
||||
4. **Welle 4:** Tender-Mapping (Requirement → Control → Finding → Verdict)
|
||||
@@ -0,0 +1,20 @@
|
||||
# CodeQL Query: Cache and Export Leak
|
||||
|
||||
## Ziel
|
||||
Finde Leaks sensibler Daten in Caches, Files, Reports und Exportpfaden.
|
||||
|
||||
## Sources
|
||||
- Sensitive payment attributes (pan, cvv, track2)
|
||||
- Full transaction objects with sensitive fields
|
||||
|
||||
## Sinks
|
||||
- Redis/Memcache writes
|
||||
- Temp file writes
|
||||
- CSV/PDF/Excel exports
|
||||
- Report builders
|
||||
|
||||
## Mapped Controls
|
||||
- `DATA-004`: Temporaere Speicher ohne sensitive Daten
|
||||
- `DATA-005`: Sensitive Daten in Telemetrie nicht offengelegt
|
||||
- `DATA-011`: Batch/Queue ohne unnoetige sensitive Felder
|
||||
- `REPORT-005`: Berichte beruecksichtigen Zeitzonen konsistent
|
||||
@@ -0,0 +1,32 @@
|
||||
# CodeQL Query: Sensitive Data to Logs
|
||||
|
||||
## Ziel
|
||||
Finde Fluesse von sensitiven Zahlungsdaten zu Loggern.
|
||||
|
||||
## Sources
|
||||
Variablen, Felder, Parameter oder JSON-Felder mit Namen:
|
||||
- `pan`, `cardNumber`, `card_number`
|
||||
- `cvv`, `cvc`
|
||||
- `track2`, `track_2`
|
||||
- `pin`
|
||||
- `expiry`, `ablauf`
|
||||
|
||||
## Sinks
|
||||
- Logger-Aufrufe (`logging.*`, `logger.*`, `console.*`, `log.*`)
|
||||
- Telemetrie-/Tracing-Emitter (`span.set_attribute`, `tracer.*)
|
||||
- Audit-Logger (wenn nicht maskiert)
|
||||
|
||||
## Expected Result
|
||||
| Field | Type |
|
||||
|-------|------|
|
||||
| file | string |
|
||||
| line | int |
|
||||
| source_name | string |
|
||||
| sink_call | string |
|
||||
| path | string[] |
|
||||
|
||||
## Mapped Controls
|
||||
- `LOG-001`: Keine sensitiven Zahlungsdaten im Log
|
||||
- `LOG-002`: PAN maskiert in Logs
|
||||
- `DATA-013`: Sensitive Daten in Telemetrie nicht offengelegt
|
||||
- `TELEMETRY-001`: Telemetriedaten ohne sensitive Zahlungsdaten
|
||||
@@ -0,0 +1,19 @@
|
||||
# CodeQL Query: Sensitive Data to HTTP Response
|
||||
|
||||
## Ziel
|
||||
Finde Fluesse sensibler Daten in HTTP-/API-Responses oder Exception-Bodies.
|
||||
|
||||
## Sources
|
||||
- Sensible Payment-Felder: pan, cvv, track2, cardNumber, pin, expiry
|
||||
- Interne Payment DTOs mit sensitiven Attributen
|
||||
|
||||
## Sinks
|
||||
- JSON serializer / response builder
|
||||
- Exception payload / error handler response
|
||||
- Template rendering output
|
||||
|
||||
## Mapped Controls
|
||||
- `API-009`: API-Antworten minimieren sensible Daten
|
||||
- `API-015`: Interne Fehler ohne sensitive Daten an Client
|
||||
- `ERROR-005`: Ausnahmebehandlung gibt keine sensitiven Rohdaten zurueck
|
||||
- `REPORT-006`: Reports offenbaren nur rollenerforderliche Daten
|
||||
@@ -0,0 +1,19 @@
|
||||
# CodeQL Query: Sensitive Data to Telemetry
|
||||
|
||||
## Ziel
|
||||
Finde Fluesse sensibler Daten in Metriken, Traces und Telemetrie-Events.
|
||||
|
||||
## Sources
|
||||
- Payment DTO fields (pan, cvv, track2, cardNumber)
|
||||
- Token/Session related fields
|
||||
|
||||
## Sinks
|
||||
- Span attributes / trace tags
|
||||
- Metric labels
|
||||
- Telemetry events / exporters
|
||||
|
||||
## Mapped Controls
|
||||
- `TELEMETRY-001`: Telemetriedaten ohne sensitive Zahlungsdaten
|
||||
- `TELEMETRY-002`: Tracing maskiert identifizierende Felder
|
||||
- `TELEMETRY-003`: Metriken ohne hochkartesische sensitive Labels
|
||||
- `DATA-013`: Sensitive Daten in Telemetrie nicht offengelegt
|
||||
@@ -0,0 +1,21 @@
|
||||
# CodeQL Query: Tenant Context Loss
|
||||
|
||||
## Ziel
|
||||
Finde Datenbank-, Cache- oder Exportpfade ohne durchgehenden Tenant-Kontext.
|
||||
|
||||
## Sources
|
||||
- Request tenant (header, token, session)
|
||||
- Device tenant
|
||||
- User tenant
|
||||
|
||||
## Danger Patterns
|
||||
- DB Query ohne tenant filter / WHERE clause
|
||||
- Cache key ohne tenant prefix
|
||||
- Export job ohne tenant binding
|
||||
- Report query ohne Mandanteneinschraenkung
|
||||
|
||||
## Mapped Controls
|
||||
- `TENANT-001`: Mandantenkontext serverseitig validiert
|
||||
- `TENANT-002`: Datenabfragen mandantenbeschraenkt
|
||||
- `TENANT-006`: Caching beruecksichtigt Mandantenkontext
|
||||
- `TENANT-008`: Datenexporte erzwingen Mandantenisolation
|
||||
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"title": "Payment Compliance Finding",
|
||||
"type": "object",
|
||||
"required": ["control_id", "engine", "status", "confidence", "evidence", "verdict_text"],
|
||||
"properties": {
|
||||
"control_id": { "type": "string" },
|
||||
"engine": {
|
||||
"type": "string",
|
||||
"enum": ["semgrep", "codeql", "contract_test", "state_machine_test", "integration_test", "manual"]
|
||||
},
|
||||
"status": {
|
||||
"type": "string",
|
||||
"enum": ["passed", "failed", "warning", "not_tested", "needs_manual_review"]
|
||||
},
|
||||
"confidence": { "type": "number", "minimum": 0, "maximum": 1 },
|
||||
"severity": {
|
||||
"type": "string",
|
||||
"enum": ["low", "medium", "high", "critical"]
|
||||
},
|
||||
"evidence": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"file": { "type": "string" },
|
||||
"line": { "type": "integer" },
|
||||
"snippet_type": { "type": "string" },
|
||||
"scenario": { "type": "string" },
|
||||
"observed_state": { "type": "string" },
|
||||
"expected_state": { "type": "string" },
|
||||
"notes": { "type": "string" }
|
||||
},
|
||||
"additionalProperties": true
|
||||
}
|
||||
},
|
||||
"mapped_requirements": {
|
||||
"type": "array",
|
||||
"items": { "type": "string" }
|
||||
},
|
||||
"verdict_text": { "type": "string" },
|
||||
"next_action": { "type": "string" }
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
rules:
|
||||
- id: payment-debug-route
|
||||
message: Debug- oder Diagnosepfad im produktiven API-Code pruefen.
|
||||
severity: WARNING
|
||||
languages: [python, javascript, typescript, java, go]
|
||||
pattern-regex: (?i)(/debug|/internal|/test|/actuator|/swagger|/openapi)
|
||||
|
||||
- id: payment-admin-route-without-auth
|
||||
message: Administrative Route ohne offensichtlichen Auth-Schutz pruefen.
|
||||
severity: WARNING
|
||||
languages: [python]
|
||||
patterns:
|
||||
- pattern: |
|
||||
@app.$METHOD($ROUTE)
|
||||
def $FUNC(...):
|
||||
...
|
||||
- metavariable-pattern:
|
||||
metavariable: $ROUTE
|
||||
pattern-regex: (?i).*(admin|config|terminal|maintenance|device|key).*
|
||||
|
||||
- id: payment-raw-exception-response
|
||||
message: Roh-Exceptions duerfen nicht direkt an Clients zurueckgegeben werden.
|
||||
severity: ERROR
|
||||
languages: [python, javascript, typescript]
|
||||
pattern-regex: (?i)(return .*str\(e\)|res\.status\(500\)\.send\(e|json\(.*error.*e)
|
||||
|
||||
- id: payment-missing-input-validation
|
||||
message: Zahlungsrelevanter Endpunkt ohne offensichtliche Validierung pruefen.
|
||||
severity: INFO
|
||||
languages: [python, javascript, typescript]
|
||||
pattern-regex: (?i)(amount|currency|terminalId|transactionId)
|
||||
|
||||
- id: payment-idor-risk
|
||||
message: Direkter Zugriff ueber terminalId/transactionId ohne Pruefung.
|
||||
severity: WARNING
|
||||
languages: [python, javascript, typescript, java, go]
|
||||
pattern-regex: (?i)(get.*terminalId|find.*terminalId|get.*transactionId|find.*transactionId)
|
||||
@@ -0,0 +1,30 @@
|
||||
rules:
|
||||
- id: payment-prod-config-test-endpoint
|
||||
message: Test- oder Sandbox-Endpunkt in produktionsnaher Konfiguration erkannt.
|
||||
severity: ERROR
|
||||
languages: [yaml, json]
|
||||
pattern-regex: (?i)(sandbox|test-endpoint|mock-terminal|dummy-acquirer)
|
||||
|
||||
- id: payment-prod-debug-flag
|
||||
message: Unsicherer Debug-Flag in Konfiguration erkannt.
|
||||
severity: WARNING
|
||||
languages: [yaml, json]
|
||||
pattern-regex: (?i)(debug:\s*true|"debug"\s*:\s*true)
|
||||
|
||||
- id: payment-open-cors
|
||||
message: Offene CORS-Freigabe pruefen.
|
||||
severity: WARNING
|
||||
languages: [yaml, json, javascript, typescript]
|
||||
pattern-regex: (?i)(Access-Control-Allow-Origin.*\*|origin:\s*["']\*["'])
|
||||
|
||||
- id: payment-insecure-session-cookie
|
||||
message: Unsicher gesetzte Session-Cookies pruefen.
|
||||
severity: ERROR
|
||||
languages: [javascript, typescript, python]
|
||||
pattern-regex: (?i)(httpOnly\s*:\s*false|secure\s*:\s*false|sameSite\s*:\s*["']none["'])
|
||||
|
||||
- id: payment-unbounded-retry
|
||||
message: Retry-Konfiguration scheint unbegrenzt oder zu hoch.
|
||||
severity: WARNING
|
||||
languages: [yaml, json]
|
||||
pattern-regex: (?i)(retry.*(9999|infinite|unbounded))
|
||||
@@ -0,0 +1,43 @@
|
||||
rules:
|
||||
- id: payment-no-md5-sha1
|
||||
message: Unsichere Hash-Algorithmen erkannt.
|
||||
severity: ERROR
|
||||
languages: [python, javascript, typescript, java, go]
|
||||
pattern-regex: (?i)\b(md5|sha1)\b
|
||||
|
||||
- id: payment-no-des-3des
|
||||
message: Veraltete symmetrische Verfahren erkannt.
|
||||
severity: ERROR
|
||||
languages: [python, javascript, typescript, java, go]
|
||||
pattern-regex: (?i)\b(des|3des|tripledes)\b
|
||||
|
||||
- id: payment-no-ecb
|
||||
message: ECB-Modus ist fuer sensible Daten ungeeignet.
|
||||
severity: ERROR
|
||||
languages: [python, javascript, typescript, java, go]
|
||||
pattern-regex: (?i)\becb\b
|
||||
|
||||
- id: payment-hardcoded-secret
|
||||
message: Moeglicherweise hartkodiertes Secret erkannt.
|
||||
severity: ERROR
|
||||
languages: [python, javascript, typescript, java, go]
|
||||
patterns:
|
||||
- pattern-either:
|
||||
- pattern: $KEY = "..."
|
||||
- pattern: const $KEY = "..."
|
||||
- pattern: final String $KEY = "..."
|
||||
- metavariable-pattern:
|
||||
metavariable: $KEY
|
||||
pattern-regex: (?i).*(secret|apikey|api_key|password|passwd|privatekey|private_key|terminalkey|zvtkey|opiKey).*
|
||||
|
||||
- id: payment-weak-random
|
||||
message: Nicht-kryptographischer Zufall in Sicherheitskontext erkannt.
|
||||
severity: ERROR
|
||||
languages: [python, javascript, typescript, java]
|
||||
pattern-regex: (?i)(Math\.random|random\.random|new Random\()
|
||||
|
||||
- id: payment-disable-tls-verify
|
||||
message: TLS-Zertifikatspruefung scheint deaktiviert zu sein.
|
||||
severity: ERROR
|
||||
languages: [python, javascript, typescript, java, go]
|
||||
pattern-regex: (?i)(verify\s*=\s*False|rejectUnauthorized\s*:\s*false|InsecureSkipVerify\s*:\s*true|trustAll)
|
||||
@@ -0,0 +1,30 @@
|
||||
rules:
|
||||
- id: payment-sensitive-in-telemetry
|
||||
message: Sensitive Zahlungsdaten in Telemetrie oder Tracing pruefen.
|
||||
severity: ERROR
|
||||
languages: [python, javascript, typescript, java, go]
|
||||
pattern-regex: (?i)(trace|span|metric|telemetry).*(pan|cvv|track2|cardnumber|pin|expiry)
|
||||
|
||||
- id: payment-sensitive-in-cache
|
||||
message: Sensitiver Wert in Cache-Key oder Cache-Payload pruefen.
|
||||
severity: WARNING
|
||||
languages: [python, javascript, typescript, java, go]
|
||||
pattern-regex: (?i)(cache|redis|memcache).*(pan|cvv|track2|cardnumber|pin)
|
||||
|
||||
- id: payment-sensitive-export
|
||||
message: Export oder Report mit sensitiven Feldern pruefen.
|
||||
severity: WARNING
|
||||
languages: [python, javascript, typescript, java, go]
|
||||
pattern-regex: (?i)(export|report|csv|xlsx|pdf).*(pan|cvv|track2|cardnumber|pin)
|
||||
|
||||
- id: payment-test-fixture-real-data
|
||||
message: Testdaten mit moeglichen echten Kartendaten pruefen.
|
||||
severity: WARNING
|
||||
languages: [json, yaml, python, javascript, typescript]
|
||||
pattern-regex: (?i)(4111111111111111|5555555555554444|track2|cvv)
|
||||
|
||||
- id: payment-queue-sensitive-payload
|
||||
message: Queue-Nachricht mit sensitiven Zahlungsfeldern pruefen.
|
||||
severity: WARNING
|
||||
languages: [python, javascript, typescript, java, go]
|
||||
pattern-regex: (?i)(publish|send|enqueue).*(pan|cvv|track2|cardnumber|pin)
|
||||
@@ -0,0 +1,42 @@
|
||||
rules:
|
||||
- id: payment-no-sensitive-logging-python
|
||||
message: Sensitive Zahlungsdaten duerfen nicht geloggt werden.
|
||||
severity: ERROR
|
||||
languages: [python]
|
||||
patterns:
|
||||
- pattern-either:
|
||||
- pattern: logging.$METHOD(..., $X, ...)
|
||||
- pattern: logger.$METHOD(..., $X, ...)
|
||||
- metavariable-pattern:
|
||||
metavariable: $X
|
||||
pattern-regex: (?i).*(pan|cvv|cvc|track2|track_2|cardnumber|card_number|karten|pin|expiry|ablauf).*
|
||||
|
||||
- id: payment-no-sensitive-logging-js
|
||||
message: Sensitive Zahlungsdaten duerfen nicht geloggt werden.
|
||||
severity: ERROR
|
||||
languages: [javascript, typescript]
|
||||
patterns:
|
||||
- pattern-either:
|
||||
- pattern: console.$METHOD(..., $X, ...)
|
||||
- pattern: logger.$METHOD(..., $X, ...)
|
||||
- metavariable-pattern:
|
||||
metavariable: $X
|
||||
pattern-regex: (?i).*(pan|cvv|cvc|track2|cardnumber|pin|expiry).*
|
||||
|
||||
- id: payment-no-token-logging
|
||||
message: Tokens oder Session-IDs duerfen nicht geloggt werden.
|
||||
severity: ERROR
|
||||
languages: [python, javascript, typescript, java, go]
|
||||
pattern-regex: (?i)(log|logger|logging|console)\.(debug|info|warn|error).*?(token|sessionid|session_id|authheader|authorization)
|
||||
|
||||
- id: payment-no-debug-logging-prod-flag
|
||||
message: Debug-Logging darf in produktiven Pfaden nicht fest aktiviert sein.
|
||||
severity: WARNING
|
||||
languages: [python, javascript, typescript, java, go]
|
||||
pattern-regex: (?i)(DEBUG\s*=\s*true|debug\s*:\s*true|setLevel\(.*DEBUG.*\))
|
||||
|
||||
- id: payment-audit-log-admin-action
|
||||
message: Administrative sicherheitsrelevante Aktion ohne Audit-Hinweis pruefen.
|
||||
severity: INFO
|
||||
languages: [python, javascript, typescript]
|
||||
pattern-regex: (?i)(deleteTerminal|rotateKey|updateConfig|disableDevice|enableMaintenance)
|
||||
@@ -0,0 +1,25 @@
|
||||
# Terminal State Machine Invariants
|
||||
|
||||
## Invariant 1
|
||||
APPROVED darf ohne expliziten Reversal-Pfad nicht in WAITING_FOR_TERMINAL zurueckgehen.
|
||||
|
||||
## Invariant 2
|
||||
DECLINED darf keinen Buchungserfolg oder Success-Report erzeugen.
|
||||
|
||||
## Invariant 3
|
||||
duplicate_response darf keinen zweiten Commit und keine zweite Success-Bestaetigung erzeugen.
|
||||
|
||||
## Invariant 4
|
||||
DESYNC muss Audit-Logging und Klaerungsstatus ausloesen.
|
||||
|
||||
## Invariant 5
|
||||
REVERSAL_PENDING darf nicht mehrfach parallel ausgeloest werden.
|
||||
|
||||
## Invariant 6
|
||||
invalid_command darf nie zu APPROVED fuehren.
|
||||
|
||||
## Invariant 7
|
||||
terminal_timeout darf nie stillschweigend als Erfolg interpretiert werden.
|
||||
|
||||
## Invariant 8
|
||||
Late responses nach finalem Zustand muessen kontrolliert behandelt werden.
|
||||
@@ -0,0 +1,47 @@
|
||||
# Terminal Payment State Machine
|
||||
|
||||
## States
|
||||
- IDLE
|
||||
- SESSION_OPEN
|
||||
- PAYMENT_REQUESTED
|
||||
- WAITING_FOR_TERMINAL
|
||||
- APPROVED
|
||||
- DECLINED
|
||||
- CANCELLED
|
||||
- REVERSAL_PENDING
|
||||
- REVERSED
|
||||
- ERROR
|
||||
- DESYNC
|
||||
|
||||
## Events
|
||||
- open_session
|
||||
- close_session
|
||||
- send_payment
|
||||
- terminal_ack
|
||||
- terminal_approve
|
||||
- terminal_decline
|
||||
- terminal_timeout
|
||||
- backend_timeout
|
||||
- reconnect
|
||||
- cancel_request
|
||||
- reversal_request
|
||||
- reversal_success
|
||||
- reversal_fail
|
||||
- duplicate_response
|
||||
- invalid_command
|
||||
|
||||
## Transitions
|
||||
| From | Event | To |
|
||||
|------|-------|----|
|
||||
| IDLE | open_session | SESSION_OPEN |
|
||||
| SESSION_OPEN | send_payment | PAYMENT_REQUESTED |
|
||||
| PAYMENT_REQUESTED | terminal_ack | WAITING_FOR_TERMINAL |
|
||||
| WAITING_FOR_TERMINAL | terminal_approve | APPROVED |
|
||||
| WAITING_FOR_TERMINAL | terminal_decline | DECLINED |
|
||||
| WAITING_FOR_TERMINAL | terminal_timeout | DESYNC |
|
||||
| WAITING_FOR_TERMINAL | cancel_request | CANCELLED |
|
||||
| APPROVED | reversal_request | REVERSAL_PENDING |
|
||||
| REVERSAL_PENDING | reversal_success | REVERSED |
|
||||
| REVERSAL_PENDING | reversal_fail | ERROR |
|
||||
| * | invalid_command | ERROR |
|
||||
| * | backend_timeout | DESYNC |
|
||||
@@ -0,0 +1,92 @@
|
||||
[
|
||||
{
|
||||
"test_id": "ZVT-SM-001",
|
||||
"name": "Duplicate approved response",
|
||||
"initial_state": "WAITING_FOR_TERMINAL",
|
||||
"events": ["terminal_approve", "duplicate_response"],
|
||||
"expected_final_state": "APPROVED",
|
||||
"invariants": ["Invariant 3"],
|
||||
"mapped_controls": ["TRANS-004", "TRANS-009", "ZVT-RESP-005"]
|
||||
},
|
||||
{
|
||||
"test_id": "ZVT-SM-002",
|
||||
"name": "Timeout then late success",
|
||||
"initial_state": "WAITING_FOR_TERMINAL",
|
||||
"events": ["terminal_timeout", "terminal_approve"],
|
||||
"expected_final_state": "DESYNC",
|
||||
"invariants": ["Invariant 4", "Invariant 7", "Invariant 8"],
|
||||
"mapped_controls": ["TRANS-005", "TRANS-007", "TERMSYNC-009", "TERMSYNC-010"]
|
||||
},
|
||||
{
|
||||
"test_id": "ZVT-SM-003",
|
||||
"name": "Decline must not produce booking",
|
||||
"initial_state": "WAITING_FOR_TERMINAL",
|
||||
"events": ["terminal_decline"],
|
||||
"expected_final_state": "DECLINED",
|
||||
"invariants": ["Invariant 2"],
|
||||
"mapped_controls": ["TRANS-011", "TRANS-025", "ZVT-RESP-002"]
|
||||
},
|
||||
{
|
||||
"test_id": "ZVT-SM-004",
|
||||
"name": "Invalid reversal before approval",
|
||||
"initial_state": "PAYMENT_REQUESTED",
|
||||
"events": ["reversal_request"],
|
||||
"expected_final_state": "ERROR",
|
||||
"invariants": ["Invariant 6"],
|
||||
"mapped_controls": ["ZVT-REV-001", "ZVT-STATE-002", "ZVT-CMD-001"]
|
||||
},
|
||||
{
|
||||
"test_id": "ZVT-SM-005",
|
||||
"name": "Cancel during waiting",
|
||||
"initial_state": "WAITING_FOR_TERMINAL",
|
||||
"events": ["cancel_request"],
|
||||
"expected_final_state": "CANCELLED",
|
||||
"invariants": ["Invariant 7"],
|
||||
"mapped_controls": ["TRANS-006", "ZVT-CMD-001", "ZVT-STATE-003"]
|
||||
},
|
||||
{
|
||||
"test_id": "ZVT-SM-006",
|
||||
"name": "Backend timeout after terminal ack",
|
||||
"initial_state": "WAITING_FOR_TERMINAL",
|
||||
"events": ["terminal_ack", "backend_timeout"],
|
||||
"expected_final_state": "DESYNC",
|
||||
"invariants": ["Invariant 4", "Invariant 7"],
|
||||
"mapped_controls": ["TERMSYNC-010", "TRANS-012", "ZVT-SESSION-003"]
|
||||
},
|
||||
{
|
||||
"test_id": "ZVT-SM-007",
|
||||
"name": "Parallel reversal requests",
|
||||
"initial_state": "APPROVED",
|
||||
"events": ["reversal_request", "reversal_request"],
|
||||
"expected_final_state": "REVERSAL_PENDING",
|
||||
"invariants": ["Invariant 5"],
|
||||
"mapped_controls": ["ZVT-REV-003", "TRANS-016", "TRANS-019"]
|
||||
},
|
||||
{
|
||||
"test_id": "ZVT-SM-008",
|
||||
"name": "Unknown response code",
|
||||
"initial_state": "WAITING_FOR_TERMINAL",
|
||||
"events": ["terminal_ack", "invalid_command"],
|
||||
"expected_final_state": "ERROR",
|
||||
"invariants": ["Invariant 6"],
|
||||
"mapped_controls": ["ZVT-RESP-003", "ZVT-COM-005", "ZVT-STATE-005"]
|
||||
},
|
||||
{
|
||||
"test_id": "ZVT-SM-009",
|
||||
"name": "Reconnect and resume controlled",
|
||||
"initial_state": "SESSION_OPEN",
|
||||
"events": ["send_payment", "terminal_timeout", "reconnect"],
|
||||
"expected_final_state": "WAITING_FOR_TERMINAL",
|
||||
"invariants": ["Invariant 7"],
|
||||
"mapped_controls": ["ZVT-SESSION-004", "TRANS-007", "ZVT-RT-004"]
|
||||
},
|
||||
{
|
||||
"test_id": "ZVT-SM-010",
|
||||
"name": "Late response after cancel",
|
||||
"initial_state": "WAITING_FOR_TERMINAL",
|
||||
"events": ["cancel_request", "terminal_approve"],
|
||||
"expected_final_state": "DESYNC",
|
||||
"invariants": ["Invariant 4", "Invariant 8"],
|
||||
"mapped_controls": ["TERMSYNC-008", "TERMSYNC-009", "TRANS-018"]
|
||||
}
|
||||
]
|
||||
@@ -5,13 +5,13 @@
|
||||
"id": "dsgvo",
|
||||
"file": "dsgvo_v2.json",
|
||||
"version": "1.0",
|
||||
"count": 80
|
||||
"count": 84
|
||||
},
|
||||
{
|
||||
"id": "ai_act",
|
||||
"file": "ai_act_v2.json",
|
||||
"version": "1.0",
|
||||
"count": 60
|
||||
"count": 81
|
||||
},
|
||||
{
|
||||
"id": "nis2",
|
||||
@@ -54,8 +54,20 @@
|
||||
"file": "dora_v2.json",
|
||||
"version": "1.0",
|
||||
"count": 20
|
||||
},
|
||||
{
|
||||
"id": "betrvg",
|
||||
"file": "betrvg_v2.json",
|
||||
"version": "1.0",
|
||||
"count": 12
|
||||
},
|
||||
{
|
||||
"id": "agg",
|
||||
"file": "agg_v2.json",
|
||||
"version": "1.0",
|
||||
"count": 8
|
||||
}
|
||||
],
|
||||
"tom_mapping_file": "_tom_mapping.json",
|
||||
"total_obligations": 325
|
||||
"total_obligations": 370
|
||||
}
|
||||
140
ai-compliance-sdk/policies/obligations/v2/agg_v2.json
Normal file
140
ai-compliance-sdk/policies/obligations/v2/agg_v2.json
Normal file
@@ -0,0 +1,140 @@
|
||||
{
|
||||
"regulation": "agg",
|
||||
"regulation_full_name": "Allgemeines Gleichbehandlungsgesetz (AGG)",
|
||||
"version": "1.0",
|
||||
"obligations": [
|
||||
{
|
||||
"id": "AGG-OBL-001",
|
||||
"title": "Diskriminierungsfreie Gestaltung von KI-Auswahlverfahren",
|
||||
"description": "KI-gestuetzte Auswahlverfahren (Recruiting, Befoerderung, Kuendigung) muessen so gestaltet sein, dass keine Benachteiligung nach § 1 AGG Merkmalen (Geschlecht, Alter, ethnische Herkunft, Religion, Behinderung, sexuelle Identitaet) erfolgt.",
|
||||
"applies_when": "AI system used in employment decisions",
|
||||
"applies_when_condition": { "all_of": [{ "field": "organization.country", "operator": "EQUALS", "value": "DE" }, { "field": "hr_context.automated_screening", "operator": "EQUALS", "value": true }] },
|
||||
"legal_basis": [{ "norm": "AGG", "article": "§ 1, § 7", "title": "Benachteiligungsverbot" }, { "norm": "AGG", "article": "§ 11", "title": "Ausschreibung" }],
|
||||
"sources": [{ "type": "national_law", "ref": "§ 1, § 7, § 11 AGG" }],
|
||||
"category": "Governance",
|
||||
"responsible": "HR / Compliance",
|
||||
"deadline": { "type": "on_event", "event": "Vor Einsatz im Auswahlverfahren" },
|
||||
"sanctions": { "description": "Schadensersatz bis 3 Monatsgehaelter (§ 15 AGG), Beweislastumkehr (§ 22 AGG)" },
|
||||
"evidence": [{ "name": "Bias-Audit-Bericht", "required": true }, "AGG-Konformitaetspruefung"],
|
||||
"priority": "kritisch",
|
||||
"tom_control_ids": ["TOM.FAIR.01"],
|
||||
"breakpilot_feature": "/sdk/use-cases",
|
||||
"valid_from": "2006-08-18",
|
||||
"valid_until": null,
|
||||
"version": "1.0"
|
||||
},
|
||||
{
|
||||
"id": "AGG-OBL-002",
|
||||
"title": "Keine Nutzung von Proxy-Merkmalen fuer Diskriminierung",
|
||||
"description": "Das KI-System darf keine Proxy-Merkmale verwenden, die indirekt auf geschuetzte Kategorien schliessen lassen (z.B. Name → Herkunft, Foto → Alter/Geschlecht, PLZ → sozialer Hintergrund).",
|
||||
"applies_when": "AI processes applicant data with identifiable features",
|
||||
"applies_when_condition": { "all_of": [{ "field": "organization.country", "operator": "EQUALS", "value": "DE" }, { "field": "hr_context.agg_categories_visible", "operator": "EQUALS", "value": true }] },
|
||||
"legal_basis": [{ "norm": "AGG", "article": "§ 3 Abs. 2", "title": "Mittelbare Benachteiligung" }],
|
||||
"sources": [{ "type": "national_law", "ref": "§ 3 Abs. 2 AGG" }],
|
||||
"category": "Technisch",
|
||||
"responsible": "Data Science / Compliance",
|
||||
"priority": "kritisch",
|
||||
"evidence": [{ "name": "Feature-Analyse-Dokumentation (keine Proxy-Merkmale)", "required": true }],
|
||||
"tom_control_ids": ["TOM.FAIR.01"],
|
||||
"valid_from": "2006-08-18",
|
||||
"version": "1.0"
|
||||
},
|
||||
{
|
||||
"id": "AGG-OBL-003",
|
||||
"title": "Beweislast-Dokumentation fuehren (§ 22 AGG)",
|
||||
"description": "Bei Indizien fuer eine Benachteiligung kehrt sich die Beweislast um (§ 22 AGG). Der Arbeitgeber muss beweisen, dass KEINE Diskriminierung vorliegt. Daher ist lueckenlose Dokumentation der KI-Entscheidungslogik zwingend.",
|
||||
"applies_when": "AI supports employment decisions in Germany",
|
||||
"applies_when_condition": { "all_of": [{ "field": "organization.country", "operator": "EQUALS", "value": "DE" }, { "field": "data_types.employee_data", "operator": "EQUALS", "value": true }] },
|
||||
"legal_basis": [{ "norm": "AGG", "article": "§ 22", "title": "Beweislast" }],
|
||||
"sources": [{ "type": "national_law", "ref": "§ 22 AGG" }],
|
||||
"category": "Governance",
|
||||
"responsible": "HR / Legal",
|
||||
"priority": "kritisch",
|
||||
"deadline": { "type": "recurring", "interval": "laufend" },
|
||||
"sanctions": { "description": "Ohne Dokumentation kann Beweislastumkehr nicht abgewehrt werden — Schadensersatz nach § 15 AGG" },
|
||||
"evidence": [{ "name": "Entscheidungsprotokoll mit KI-Begruendung", "required": true }, "Audit-Trail aller KI-Bewertungen"],
|
||||
"tom_control_ids": ["TOM.LOG.01", "TOM.GOV.01"],
|
||||
"valid_from": "2006-08-18",
|
||||
"version": "1.0"
|
||||
},
|
||||
{
|
||||
"id": "AGG-OBL-004",
|
||||
"title": "Regelmaessige Bias-Audits bei KI-gestuetzter Personalauswahl",
|
||||
"description": "KI-Systeme im Recruiting muessen regelmaessig auf Bias geprueft werden: statistische Analyse der Ergebnisse nach Geschlecht, Altersgruppen und soweit zulaessig nach Herkunft.",
|
||||
"applies_when": "AI ranks or scores candidates",
|
||||
"applies_when_condition": { "all_of": [{ "field": "organization.country", "operator": "EQUALS", "value": "DE" }, { "field": "hr_context.candidate_ranking", "operator": "EQUALS", "value": true }] },
|
||||
"legal_basis": [{ "norm": "AGG", "article": "§ 1, § 3", "title": "Unmittelbare und mittelbare Benachteiligung" }],
|
||||
"category": "Technisch",
|
||||
"responsible": "Data Science",
|
||||
"priority": "hoch",
|
||||
"deadline": { "type": "recurring", "interval": "quartalsweise" },
|
||||
"evidence": [{ "name": "Bias-Audit-Ergebnis (letzte 3 Monate)", "required": true }],
|
||||
"tom_control_ids": ["TOM.FAIR.01"],
|
||||
"valid_from": "2006-08-18",
|
||||
"version": "1.0"
|
||||
},
|
||||
{
|
||||
"id": "AGG-OBL-005",
|
||||
"title": "Schulung der HR-Entscheider ueber KI-Grenzen",
|
||||
"description": "Personen, die KI-gestuetzte Empfehlungen im Personalbereich nutzen, muessen ueber Systemgrenzen, Bias-Risiken und ihre Pflicht zur eigenstaendigen Pruefung geschult werden.",
|
||||
"applies_when": "AI provides recommendations for HR decisions",
|
||||
"applies_when_condition": { "all_of": [{ "field": "organization.country", "operator": "EQUALS", "value": "DE" }, { "field": "data_types.employee_data", "operator": "EQUALS", "value": true }] },
|
||||
"legal_basis": [{ "norm": "AGG", "article": "§ 12 Abs. 2", "title": "Pflicht des Arbeitgebers zu Schutzmassnahmen" }],
|
||||
"category": "Organisatorisch",
|
||||
"responsible": "HR / Training",
|
||||
"priority": "hoch",
|
||||
"deadline": { "type": "recurring", "interval": "jaehrlich" },
|
||||
"evidence": [{ "name": "Schulungsnachweis AGG + KI-Kompetenz", "required": true }],
|
||||
"tom_control_ids": [],
|
||||
"valid_from": "2006-08-18",
|
||||
"version": "1.0"
|
||||
},
|
||||
{
|
||||
"id": "AGG-OBL-006",
|
||||
"title": "Beschwerdemechanismus fuer abgelehnte Bewerber",
|
||||
"description": "Bewerber muessen die Moeglichkeit haben, sich ueber KI-gestuetzte Auswahlentscheidungen zu beschweren. Die zustaendige Stelle (§ 13 AGG) muss benannt sein.",
|
||||
"applies_when": "AI used in applicant selection process",
|
||||
"applies_when_condition": { "all_of": [{ "field": "organization.country", "operator": "EQUALS", "value": "DE" }, { "field": "hr_context.automated_screening", "operator": "EQUALS", "value": true }] },
|
||||
"legal_basis": [{ "norm": "AGG", "article": "§ 13", "title": "Beschwerderecht" }],
|
||||
"category": "Organisatorisch",
|
||||
"responsible": "HR",
|
||||
"priority": "hoch",
|
||||
"evidence": [{ "name": "Dokumentierter Beschwerdemechanismus", "required": true }],
|
||||
"tom_control_ids": [],
|
||||
"valid_from": "2006-08-18",
|
||||
"version": "1.0"
|
||||
},
|
||||
{
|
||||
"id": "AGG-OBL-007",
|
||||
"title": "Schadensersatzrisiko dokumentieren und versichern",
|
||||
"description": "Das Schadensersatzrisiko bei AGG-Verstoessen (bis 3 Monatsgehaelter pro Fall, § 15 AGG) muss bewertet und dokumentiert werden. Bei hohem Bewerbungsvolumen kann das kumulierte Risiko erheblich sein.",
|
||||
"applies_when": "AI processes high volume of applications",
|
||||
"applies_when_condition": { "all_of": [{ "field": "organization.country", "operator": "EQUALS", "value": "DE" }, { "field": "hr_context.automated_screening", "operator": "EQUALS", "value": true }] },
|
||||
"legal_basis": [{ "norm": "AGG", "article": "§ 15", "title": "Entschaedigung und Schadensersatz" }],
|
||||
"category": "Governance",
|
||||
"responsible": "Legal / Finance",
|
||||
"priority": "hoch",
|
||||
"evidence": [{ "name": "Risikobewertung AGG-Schadensersatz", "required": false }],
|
||||
"tom_control_ids": [],
|
||||
"valid_from": "2006-08-18",
|
||||
"version": "1.0"
|
||||
},
|
||||
{
|
||||
"id": "AGG-OBL-008",
|
||||
"title": "KI-Stellenausschreibungen AGG-konform gestalten",
|
||||
"description": "Wenn KI bei der Erstellung oder Optimierung von Stellenausschreibungen eingesetzt wird, muss sichergestellt sein, dass die Ausschreibungen keine diskriminierenden Formulierungen enthalten (§ 11 AGG).",
|
||||
"applies_when": "AI generates or optimizes job postings",
|
||||
"applies_when_condition": { "all_of": [{ "field": "organization.country", "operator": "EQUALS", "value": "DE" }] },
|
||||
"legal_basis": [{ "norm": "AGG", "article": "§ 11", "title": "Ausschreibung" }],
|
||||
"category": "Organisatorisch",
|
||||
"responsible": "HR / Marketing",
|
||||
"priority": "hoch",
|
||||
"evidence": [{ "name": "Pruefprotokoll Stellenausschreibung auf AGG-Konformitaet", "required": false }],
|
||||
"tom_control_ids": [],
|
||||
"valid_from": "2006-08-18",
|
||||
"version": "1.0"
|
||||
}
|
||||
],
|
||||
"controls": [],
|
||||
"incident_deadlines": []
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
250
ai-compliance-sdk/policies/obligations/v2/betrvg_v2.json
Normal file
250
ai-compliance-sdk/policies/obligations/v2/betrvg_v2.json
Normal file
@@ -0,0 +1,250 @@
|
||||
{
|
||||
"regulation": "betrvg",
|
||||
"regulation_full_name": "Betriebsverfassungsgesetz (BetrVG)",
|
||||
"version": "1.0",
|
||||
"obligations": [
|
||||
{
|
||||
"id": "BETRVG-OBL-001",
|
||||
"title": "Mitbestimmung bei technischen Ueberwachungseinrichtungen",
|
||||
"description": "Einfuehrung und Anwendung von technischen Einrichtungen, die dazu bestimmt sind, das Verhalten oder die Leistung der Arbeitnehmer zu ueberwachen, beduerfen der Zustimmung des Betriebsrats. Das BAG hat klargestellt, dass bereits die objektive Eignung zur Ueberwachung genuegt — eine tatsaechliche Nutzung zu diesem Zweck ist nicht erforderlich (BAG 1 ABR 20/21, 1 ABN 36/18).",
|
||||
"applies_when": "technical system can monitor employee behavior or performance",
|
||||
"applies_when_condition": { "all_of": [{ "field": "organization.country", "operator": "IN_ARRAY", "value": ["DE", "AT"] }, { "field": "data_types.employee_data", "operator": "EQUALS", "value": true }] },
|
||||
"legal_basis": [{ "norm": "BetrVG", "article": "§ 87 Abs. 1 Nr. 6", "title": "Mitbestimmung bei technischen Ueberwachungseinrichtungen" }],
|
||||
"sources": [{ "type": "national_law", "ref": "§ 87 Abs. 1 Nr. 6 BetrVG" }, { "type": "court_decision", "ref": "BAG 1 ABR 20/21 (Microsoft 365)" }, { "type": "court_decision", "ref": "BAG 1 ABN 36/18 (Standardsoftware)" }],
|
||||
"category": "Mitbestimmung",
|
||||
"responsible": "Arbeitgeber / HR",
|
||||
"deadline": { "type": "on_event", "event": "Vor Einfuehrung des Systems" },
|
||||
"sanctions": { "description": "Unterlassungsanspruch des Betriebsrats, einstweilige Verfuegung moeglich, Betriebsvereinbarung ueber Einigungsstelle erzwingbar (§ 87 Abs. 2 BetrVG)" },
|
||||
"evidence": [{ "name": "Betriebsvereinbarung oder dokumentierte Zustimmung des Betriebsrats", "required": true }, "Protokoll der Betriebsratssitzung"],
|
||||
"priority": "kritisch",
|
||||
"tom_control_ids": ["TOM.GOV.01", "TOM.AC.01"],
|
||||
"breakpilot_feature": "/sdk/betriebsvereinbarung",
|
||||
"valid_from": "1972-01-19",
|
||||
"valid_until": null,
|
||||
"version": "1.0",
|
||||
"how_to_implement": "Betriebsrat fruehzeitig informieren, gemeinsame Bewertung der Ueberwachungseignung durchfuehren, Betriebsvereinbarung mit Zweckbindung und verbotenen Nutzungen abschliessen."
|
||||
},
|
||||
{
|
||||
"id": "BETRVG-OBL-002",
|
||||
"title": "Keine Geringfuegigkeitsschwelle bei Standardsoftware",
|
||||
"description": "Auch alltaegliche Standardsoftware (Excel, Word, E-Mail-Clients) unterliegt der Mitbestimmung, wenn sie objektiv geeignet ist, Verhaltens- oder Leistungsdaten zu erheben. Es gibt keine Geringfuegigkeitsschwelle (BAG 1 ABN 36/18).",
|
||||
"applies_when": "any software used by employees that can log or track usage",
|
||||
"applies_when_condition": { "all_of": [{ "field": "organization.country", "operator": "EQUALS", "value": "DE" }, { "field": "data_types.employee_data", "operator": "EQUALS", "value": true }] },
|
||||
"legal_basis": [{ "norm": "BetrVG", "article": "§ 87 Abs. 1 Nr. 6", "title": "Mitbestimmung — keine Geringfuegigkeitsschwelle" }],
|
||||
"sources": [{ "type": "court_decision", "ref": "BAG 1 ABN 36/18" }],
|
||||
"category": "Mitbestimmung",
|
||||
"responsible": "IT-Leitung / HR",
|
||||
"deadline": { "type": "on_event", "event": "Vor Einfuehrung oder Aenderung" },
|
||||
"sanctions": { "description": "Unterlassungsanspruch, einstweilige Verfuegung" },
|
||||
"evidence": [{ "name": "Bestandsaufnahme aller IT-Systeme mit Ueberwachungseignung", "required": true }],
|
||||
"priority": "hoch",
|
||||
"tom_control_ids": [],
|
||||
"breakpilot_feature": null,
|
||||
"valid_from": "2018-10-23",
|
||||
"valid_until": null,
|
||||
"version": "1.0"
|
||||
},
|
||||
{
|
||||
"id": "BETRVG-OBL-003",
|
||||
"title": "Mitbestimmung bei Ueberwachung durch Drittsysteme (SaaS/Cloud)",
|
||||
"description": "Auch wenn die Ueberwachung ueber ein Dritt-System (SaaS, Cloud, externer Anbieter) laeuft, bleibt der Betriebsrat zu beteiligen. Die Verantwortung des Arbeitgebers entfaellt nicht durch Auslagerung (BAG 1 ABR 68/13).",
|
||||
"applies_when": "cloud or SaaS system processes employee data",
|
||||
"applies_when_condition": { "all_of": [{ "field": "organization.country", "operator": "EQUALS", "value": "DE" }, { "field": "data_types.employee_data", "operator": "EQUALS", "value": true }] },
|
||||
"legal_basis": [{ "norm": "BetrVG", "article": "§ 87 Abs. 1 Nr. 6", "title": "Mitbestimmung bei Drittsystemen" }],
|
||||
"sources": [{ "type": "court_decision", "ref": "BAG 1 ABR 68/13" }],
|
||||
"category": "Mitbestimmung",
|
||||
"responsible": "IT-Leitung / Einkauf",
|
||||
"deadline": { "type": "on_event", "event": "Vor Vertragsschluss mit SaaS-Anbieter" },
|
||||
"sanctions": { "description": "Unterlassungsanspruch" },
|
||||
"evidence": [{ "name": "Datenschutz-Folgenabschaetzung fuer Cloud-Dienst", "required": false }, "Betriebsvereinbarung"],
|
||||
"priority": "hoch",
|
||||
"tom_control_ids": ["TOM.PROC.01"],
|
||||
"breakpilot_feature": null,
|
||||
"valid_from": "2015-07-21",
|
||||
"valid_until": null,
|
||||
"version": "1.0"
|
||||
},
|
||||
{
|
||||
"id": "BETRVG-OBL-004",
|
||||
"title": "Mitbestimmung bei E-Mail- und Kommunikationssoftware",
|
||||
"description": "Sowohl Einfuehrung als auch Nutzung softwarebasierter Anwendungen fuer die E-Mail-Kommunikation sind mitbestimmungspflichtig (BAG 1 ABR 31/19). Dies gilt auch fuer Teams, Slack und vergleichbare Messenger.",
|
||||
"applies_when": "organization introduces or changes email or messaging systems",
|
||||
"applies_when_condition": { "all_of": [{ "field": "organization.country", "operator": "EQUALS", "value": "DE" }, { "field": "data_types.employee_data", "operator": "EQUALS", "value": true }] },
|
||||
"legal_basis": [{ "norm": "BetrVG", "article": "§ 87 Abs. 1 Nr. 6", "title": "Mitbestimmung bei Kommunikationssoftware" }],
|
||||
"sources": [{ "type": "court_decision", "ref": "BAG 1 ABR 31/19" }, { "type": "court_decision", "ref": "BAG 1 ABR 46/10" }],
|
||||
"category": "Mitbestimmung",
|
||||
"responsible": "IT-Leitung / HR",
|
||||
"deadline": { "type": "on_event", "event": "Vor Einfuehrung oder Funktionsaenderung" },
|
||||
"sanctions": { "description": "Unterlassungsanspruch, einstweilige Verfuegung" },
|
||||
"evidence": [{ "name": "Betriebsvereinbarung zu E-Mail-/Messaging-Nutzung", "required": true }],
|
||||
"priority": "hoch",
|
||||
"tom_control_ids": ["TOM.AC.01"],
|
||||
"breakpilot_feature": null,
|
||||
"valid_from": "2021-01-27",
|
||||
"valid_until": null,
|
||||
"version": "1.0"
|
||||
},
|
||||
{
|
||||
"id": "BETRVG-OBL-005",
|
||||
"title": "Verbot der dauerhaften Leistungsueberwachung",
|
||||
"description": "Eine dauerhafte quantitative Erfassung und Auswertung einzelner Arbeitsschritte stellt einen schwerwiegenden Eingriff in das Persoenlichkeitsrecht dar (BAG 1 ABR 46/15). Belastungsstatistiken und KPI-Dashboards auf Personenebene beduerfen besonderer Rechtfertigung.",
|
||||
"applies_when": "system provides individual performance metrics or KPIs",
|
||||
"applies_when_condition": { "all_of": [{ "field": "organization.country", "operator": "EQUALS", "value": "DE" }, { "field": "purpose.profiling", "operator": "EQUALS", "value": true }] },
|
||||
"legal_basis": [{ "norm": "BetrVG", "article": "§ 87 Abs. 1 Nr. 6", "title": "Persoenlichkeitsschutz bei Kennzahlenueberwachung" }, { "norm": "GG", "article": "Art. 2 Abs. 1 i.V.m. Art. 1 Abs. 1", "title": "Allgemeines Persoenlichkeitsrecht" }],
|
||||
"sources": [{ "type": "court_decision", "ref": "BAG 1 ABR 46/15 (Belastungsstatistik)" }],
|
||||
"category": "Mitbestimmung",
|
||||
"responsible": "HR / Compliance",
|
||||
"deadline": { "type": "recurring", "interval": "laufend" },
|
||||
"sanctions": { "description": "Unterlassungsanspruch, Schadensersatz bei Persoenlichkeitsrechtsverletzung" },
|
||||
"evidence": [{ "name": "Nachweis dass keine individuelle Leistungsueberwachung stattfindet", "required": true }],
|
||||
"priority": "kritisch",
|
||||
"tom_control_ids": ["TOM.GOV.03"],
|
||||
"breakpilot_feature": null,
|
||||
"valid_from": "2017-04-25",
|
||||
"valid_until": null,
|
||||
"version": "1.0"
|
||||
},
|
||||
{
|
||||
"id": "BETRVG-OBL-006",
|
||||
"title": "Unterrichtung bei Planung technischer Anlagen",
|
||||
"description": "Der Arbeitgeber hat den Betriebsrat ueber die Planung von technischen Anlagen rechtzeitig unter Vorlage der erforderlichen Unterlagen zu unterrichten und mit ihm zu beraten.",
|
||||
"applies_when": "organization plans new technical infrastructure",
|
||||
"applies_when_condition": { "all_of": [{ "field": "organization.country", "operator": "EQUALS", "value": "DE" }] },
|
||||
"legal_basis": [{ "norm": "BetrVG", "article": "§ 90 Abs. 1 Nr. 3", "title": "Unterrichtungs- und Beratungsrechte bei Planung" }],
|
||||
"sources": [{ "type": "national_law", "ref": "§ 90 BetrVG" }],
|
||||
"category": "Information",
|
||||
"responsible": "IT-Leitung",
|
||||
"deadline": { "type": "on_event", "event": "Rechtzeitig vor Umsetzung" },
|
||||
"sanctions": { "description": "Beratungsanspruch, ggf. Aussetzung der Massnahme" },
|
||||
"evidence": [{ "name": "Unterrichtungsschreiben an Betriebsrat mit technischer Dokumentation", "required": true }],
|
||||
"priority": "hoch",
|
||||
"tom_control_ids": [],
|
||||
"breakpilot_feature": null,
|
||||
"valid_from": "1972-01-19",
|
||||
"valid_until": null,
|
||||
"version": "1.0"
|
||||
},
|
||||
{
|
||||
"id": "BETRVG-OBL-007",
|
||||
"title": "Mitbestimmung bei Personalfrageboegen und Bewertungssystemen",
|
||||
"description": "Personalfrageboegen und allgemeine Beurteilungsgrundsaetze beduerfen der Zustimmung des Betriebsrats. Dies umfasst auch KI-gestuetzte Bewertungssysteme fuer Mitarbeiterbeurteilungen (BAG 1 ABR 40/07).",
|
||||
"applies_when": "AI or IT system supports employee evaluation or surveys",
|
||||
"applies_when_condition": { "all_of": [{ "field": "organization.country", "operator": "EQUALS", "value": "DE" }, { "field": "purpose.profiling", "operator": "EQUALS", "value": true }] },
|
||||
"legal_basis": [{ "norm": "BetrVG", "article": "§ 94", "title": "Personalfrageboegen, Beurteilungsgrundsaetze" }, { "norm": "BetrVG", "article": "§ 95", "title": "Auswahlrichtlinien" }],
|
||||
"sources": [{ "type": "court_decision", "ref": "BAG 1 ABR 40/07" }, { "type": "court_decision", "ref": "BAG 1 ABR 16/07" }],
|
||||
"category": "Mitbestimmung",
|
||||
"responsible": "HR",
|
||||
"deadline": { "type": "on_event", "event": "Vor Einfuehrung des Bewertungssystems" },
|
||||
"sanctions": { "description": "Nichtigkeit der Bewertung, Unterlassungsanspruch" },
|
||||
"evidence": [{ "name": "Betriebsvereinbarung zu Beurteilungsgrundsaetzen", "required": true }],
|
||||
"priority": "kritisch",
|
||||
"tom_control_ids": ["TOM.GOV.01"],
|
||||
"breakpilot_feature": null,
|
||||
"valid_from": "1972-01-19",
|
||||
"valid_until": null,
|
||||
"version": "1.0"
|
||||
},
|
||||
{
|
||||
"id": "BETRVG-OBL-008",
|
||||
"title": "Mitbestimmung bei KI-gestuetztem Recruiting",
|
||||
"description": "KI-Systeme im Recruiting-Prozess (CV-Screening, Ranking, Vorselektion) beruehren die Mitbestimmung bei Auswahlrichtlinien (§ 95 BetrVG) und ggf. bei Einstellungen (§ 99 BetrVG). Zusaetzlich AI Act Hochrisiko-Klassifikation (Annex III Nr. 4).",
|
||||
"applies_when": "AI system used in hiring, promotion or termination decisions",
|
||||
"applies_when_condition": { "all_of": [{ "field": "organization.country", "operator": "EQUALS", "value": "DE" }, { "field": "purpose.automation", "operator": "EQUALS", "value": true }, { "field": "data_types.employee_data", "operator": "EQUALS", "value": true }] },
|
||||
"legal_basis": [{ "norm": "BetrVG", "article": "§ 95", "title": "Auswahlrichtlinien" }, { "norm": "BetrVG", "article": "§ 99", "title": "Mitbestimmung bei personellen Einzelmassnahmen" }, { "norm": "EU AI Act", "article": "Annex III Nr. 4", "title": "Hochrisiko: Beschaeftigung" }],
|
||||
"sources": [{ "type": "national_law", "ref": "§ 95, § 99 BetrVG" }],
|
||||
"category": "Mitbestimmung",
|
||||
"responsible": "HR / Legal",
|
||||
"deadline": { "type": "on_event", "event": "Vor Einsatz im Recruiting" },
|
||||
"sanctions": { "description": "Unterlassungsanspruch, Anfechtung der Einstellung, AI Act Bussgeld bei Hochrisiko-Verstoss" },
|
||||
"evidence": [{ "name": "Betriebsvereinbarung KI im Recruiting", "required": true }, "DSFA", "AI Act Konformitaetsbewertung"],
|
||||
"priority": "kritisch",
|
||||
"tom_control_ids": ["TOM.GOV.01", "TOM.FAIR.01"],
|
||||
"breakpilot_feature": "/sdk/ai-act",
|
||||
"valid_from": "1972-01-19",
|
||||
"valid_until": null,
|
||||
"version": "1.0"
|
||||
},
|
||||
{
|
||||
"id": "BETRVG-OBL-009",
|
||||
"title": "Mitbestimmung bei Betriebsaenderungen durch KI",
|
||||
"description": "Grundlegende Aenderung der Betriebsorganisation durch KI-Einfuehrung kann eine Betriebsaenderung darstellen. In Unternehmen mit mehr als 20 wahlberechtigten Arbeitnehmern ist ein Interessenausgleich zu versuchen und ein Sozialplan aufzustellen.",
|
||||
"applies_when": "AI introduction fundamentally changes work organization",
|
||||
"applies_when_condition": { "all_of": [{ "field": "organization.country", "operator": "EQUALS", "value": "DE" }, { "field": "organization.employee_count", "operator": "GREATER_THAN", "value": 20 }] },
|
||||
"legal_basis": [{ "norm": "BetrVG", "article": "§ 111", "title": "Betriebsaenderungen" }, { "norm": "BetrVG", "article": "§ 112", "title": "Interessenausgleich, Sozialplan" }],
|
||||
"sources": [{ "type": "national_law", "ref": "§§ 111-113 BetrVG" }],
|
||||
"category": "Mitbestimmung",
|
||||
"responsible": "Geschaeftsfuehrung / HR",
|
||||
"deadline": { "type": "on_event", "event": "Rechtzeitig vor Umsetzung" },
|
||||
"sanctions": { "description": "Nachteilsausgleich, Sozialplananspruch, Anfechtung der Massnahme" },
|
||||
"evidence": [{ "name": "Interessenausgleich", "required": false }, "Sozialplan", "Unterrichtung des Betriebsrats"],
|
||||
"priority": "hoch",
|
||||
"tom_control_ids": [],
|
||||
"breakpilot_feature": null,
|
||||
"valid_from": "1972-01-19",
|
||||
"valid_until": null,
|
||||
"version": "1.0"
|
||||
},
|
||||
{
|
||||
"id": "BETRVG-OBL-010",
|
||||
"title": "Zustaendigkeit bei konzernweiten IT-Systemen",
|
||||
"description": "Bei konzernweit eingesetzten IT-Systemen (z.B. M365, SAP) kann nicht der lokale Betriebsrat, sondern der Gesamt- oder Konzernbetriebsrat zustaendig sein (BAG 1 ABR 45/11). Die Zustaendigkeitsabgrenzung ist vor Einfuehrung zu klaeren.",
|
||||
"applies_when": "IT system deployed across multiple establishments or companies",
|
||||
"applies_when_condition": { "all_of": [{ "field": "organization.country", "operator": "EQUALS", "value": "DE" }] },
|
||||
"legal_basis": [{ "norm": "BetrVG", "article": "§ 50 Abs. 1", "title": "Zustaendigkeit Gesamtbetriebsrat" }, { "norm": "BetrVG", "article": "§ 58 Abs. 1", "title": "Zustaendigkeit Konzernbetriebsrat" }],
|
||||
"sources": [{ "type": "court_decision", "ref": "BAG 1 ABR 45/11 (SAP ERP)" }, { "type": "court_decision", "ref": "BAG 1 ABR 2/05" }],
|
||||
"category": "Organisation",
|
||||
"responsible": "HR / Legal",
|
||||
"deadline": { "type": "on_event", "event": "Vor Einfuehrung" },
|
||||
"sanctions": { "description": "Unwirksamkeit der Vereinbarung bei falschem Verhandlungspartner" },
|
||||
"evidence": [{ "name": "Zustaendigkeitsbestimmung dokumentiert", "required": true }],
|
||||
"priority": "hoch",
|
||||
"tom_control_ids": [],
|
||||
"breakpilot_feature": null,
|
||||
"valid_from": "2012-09-25",
|
||||
"valid_until": null,
|
||||
"version": "1.0"
|
||||
},
|
||||
{
|
||||
"id": "BETRVG-OBL-011",
|
||||
"title": "Change-Management — erneute Mitbestimmung bei Funktionserweiterungen",
|
||||
"description": "Neue Module, Funktionen oder Konnektoren in bestehenden IT-Systemen koennen eine erneute Mitbestimmung ausloesen, wenn sie die Ueberwachungseignung aendern oder erweitern (BAG 1 ABR 20/21 — Anwendung, nicht nur Einfuehrung).",
|
||||
"applies_when": "existing IT system receives feature updates affecting monitoring capability",
|
||||
"applies_when_condition": { "all_of": [{ "field": "organization.country", "operator": "EQUALS", "value": "DE" }, { "field": "data_types.employee_data", "operator": "EQUALS", "value": true }] },
|
||||
"legal_basis": [{ "norm": "BetrVG", "article": "§ 87 Abs. 1 Nr. 6", "title": "Mitbestimmung bei Anwendung (nicht nur Einfuehrung)" }],
|
||||
"sources": [{ "type": "court_decision", "ref": "BAG 1 ABR 20/21" }],
|
||||
"category": "Mitbestimmung",
|
||||
"responsible": "IT-Leitung / HR",
|
||||
"deadline": { "type": "on_event", "event": "Vor Aktivierung neuer Funktionen" },
|
||||
"sanctions": { "description": "Unterlassungsanspruch" },
|
||||
"evidence": [{ "name": "Change-Management-Protokoll mit BR-Bewertung", "required": true }],
|
||||
"priority": "hoch",
|
||||
"tom_control_ids": [],
|
||||
"breakpilot_feature": null,
|
||||
"valid_from": "2022-03-08",
|
||||
"valid_until": null,
|
||||
"version": "1.0"
|
||||
},
|
||||
{
|
||||
"id": "BETRVG-OBL-012",
|
||||
"title": "Videoueberwachung — Mitbestimmung und Verhaeltnismaessigkeit",
|
||||
"description": "Videoueberwachung am Arbeitsplatz ist grundsaetzlich mitbestimmungspflichtig. Die Regelungen ueber Einfuehrung und Ausgestaltung beduerfen der Zustimmung des Betriebsrats (BAG 1 ABR 78/11, 1 ABR 21/03).",
|
||||
"applies_when": "organization uses video surveillance that may capture employees",
|
||||
"applies_when_condition": { "all_of": [{ "field": "organization.country", "operator": "EQUALS", "value": "DE" }, { "field": "data_protection.video_surveillance", "operator": "EQUALS", "value": true }] },
|
||||
"legal_basis": [{ "norm": "BetrVG", "article": "§ 87 Abs. 1 Nr. 6", "title": "Mitbestimmung bei Videoueberwachung" }],
|
||||
"sources": [{ "type": "court_decision", "ref": "BAG 1 ABR 78/11" }, { "type": "court_decision", "ref": "BAG 1 ABR 21/03" }],
|
||||
"category": "Mitbestimmung",
|
||||
"responsible": "Facility Management / HR",
|
||||
"deadline": { "type": "on_event", "event": "Vor Installation" },
|
||||
"sanctions": { "description": "Unterlassungsanspruch, Beweisverwertungsverbot" },
|
||||
"evidence": [{ "name": "Betriebsvereinbarung Videoueberwachung", "required": true }, "Beschilderung"],
|
||||
"priority": "kritisch",
|
||||
"tom_control_ids": ["TOM.PHY.01"],
|
||||
"breakpilot_feature": null,
|
||||
"valid_from": "2004-06-29",
|
||||
"valid_until": null,
|
||||
"version": "1.0"
|
||||
}
|
||||
],
|
||||
"controls": [],
|
||||
"incident_deadlines": []
|
||||
}
|
||||
@@ -4591,6 +4591,209 @@
|
||||
"valid_from": "2018-05-25",
|
||||
"valid_until": null,
|
||||
"version": "1.0"
|
||||
},
|
||||
{
|
||||
"id": "DSGVO-OBL-081",
|
||||
"title": "Drittlanduebermittlung nur mit geeigneten Garantien",
|
||||
"description": "Die Uebermittlung personenbezogener Daten in Drittlaender (insbesondere USA) ist nur zulaessig, wenn ein Angemessenheitsbeschluss vorliegt oder geeignete Garantien (z.B. Standardvertragsklauseln) implementiert sind. Nach Schrems II (C-311/18) muessen zusaetzliche Massnahmen geprueft werden.",
|
||||
"applies_when": "data transferred to third country or US provider used",
|
||||
"applies_when_condition": {
|
||||
"all_of": [
|
||||
{
|
||||
"field": "data_protection.processes_personal_data",
|
||||
"operator": "EQUALS",
|
||||
"value": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"legal_basis": [
|
||||
{
|
||||
"norm": "DSGVO",
|
||||
"article": "Art. 44",
|
||||
"title": "Allgemeine Grundsaetze der Datenuebermittlung"
|
||||
},
|
||||
{
|
||||
"norm": "DSGVO",
|
||||
"article": "Art. 46",
|
||||
"title": "Datenuebermittlung vorbehaltlich geeigneter Garantien"
|
||||
}
|
||||
],
|
||||
"sources": [
|
||||
{
|
||||
"type": "regulation",
|
||||
"ref": "Art. 44-49 DSGVO"
|
||||
},
|
||||
{
|
||||
"type": "court_decision",
|
||||
"ref": "EuGH C-311/18 (Schrems II)"
|
||||
}
|
||||
],
|
||||
"category": "Governance",
|
||||
"responsible": "Datenschutzbeauftragter",
|
||||
"deadline": {
|
||||
"type": "on_event",
|
||||
"event": "Vor Beginn der Datenuebermittlung"
|
||||
},
|
||||
"sanctions": {
|
||||
"max_fine": "20 Mio. EUR oder 4% Jahresumsatz"
|
||||
},
|
||||
"evidence": [
|
||||
{
|
||||
"name": "Transfer Impact Assessment (TIA)",
|
||||
"required": true
|
||||
},
|
||||
"Standardvertragsklauseln (SCC)",
|
||||
"Dokumentation zusaetzlicher Massnahmen"
|
||||
],
|
||||
"priority": "kritisch",
|
||||
"tom_control_ids": [
|
||||
"TOM.GOV.01",
|
||||
"TOM.CRY.01"
|
||||
],
|
||||
"valid_from": "2018-05-25",
|
||||
"version": "1.0"
|
||||
},
|
||||
{
|
||||
"id": "DSGVO-OBL-082",
|
||||
"title": "Transfer Impact Assessment (TIA) bei US-Anbietern",
|
||||
"description": "Bei Nutzung von US-Cloud-Anbietern (AWS, Azure, Google etc.) muss ein Transfer Impact Assessment durchgefuehrt werden, das FISA 702 und Cloud Act Risiken bewertet und dokumentiert, ob die Standardvertragsklauseln wirksam schuetzen.",
|
||||
"applies_when": "US cloud provider used for personal data",
|
||||
"applies_when_condition": {
|
||||
"all_of": [
|
||||
{
|
||||
"field": "data_protection.processes_personal_data",
|
||||
"operator": "EQUALS",
|
||||
"value": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"legal_basis": [
|
||||
{
|
||||
"norm": "DSGVO",
|
||||
"article": "Art. 46 Abs. 1",
|
||||
"title": "Geeignete Garantien"
|
||||
},
|
||||
{
|
||||
"norm": "EuGH",
|
||||
"article": "C-311/18",
|
||||
"title": "Schrems II — Wirksamkeit von SCCs pruefen"
|
||||
}
|
||||
],
|
||||
"sources": [
|
||||
{
|
||||
"type": "court_decision",
|
||||
"ref": "EuGH C-311/18 (Schrems II)"
|
||||
},
|
||||
{
|
||||
"type": "guidance",
|
||||
"ref": "EDPB Recommendations 01/2020 Supplementary Measures"
|
||||
}
|
||||
],
|
||||
"category": "Governance",
|
||||
"responsible": "Datenschutzbeauftragter / Legal",
|
||||
"deadline": {
|
||||
"type": "on_event",
|
||||
"event": "Vor Vertragsschluss mit US-Anbieter"
|
||||
},
|
||||
"sanctions": {
|
||||
"max_fine": "20 Mio. EUR oder 4% Jahresumsatz"
|
||||
},
|
||||
"evidence": [
|
||||
{
|
||||
"name": "Transfer Impact Assessment",
|
||||
"required": true
|
||||
},
|
||||
"FISA 702 Risikobewertung"
|
||||
],
|
||||
"priority": "kritisch",
|
||||
"tom_control_ids": [
|
||||
"TOM.GOV.01"
|
||||
],
|
||||
"valid_from": "2020-07-16",
|
||||
"version": "1.0"
|
||||
},
|
||||
{
|
||||
"id": "DSGVO-OBL-083",
|
||||
"title": "Zusaetzliche technische Massnahmen bei Drittlanduebermittlung",
|
||||
"description": "Wenn Standardvertragsklauseln allein nicht ausreichen (z.B. bei FISA 702 Exposure), muessen zusaetzliche technische Massnahmen implementiert werden: E2EE mit eigener Schluesselhoheit, Pseudonymisierung vor Uebermittlung, oder Verzicht auf den US-Anbieter.",
|
||||
"applies_when": "SCC alone insufficient due to surveillance laws",
|
||||
"applies_when_condition": {
|
||||
"all_of": [
|
||||
{
|
||||
"field": "data_protection.processes_personal_data",
|
||||
"operator": "EQUALS",
|
||||
"value": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"legal_basis": [
|
||||
{
|
||||
"norm": "DSGVO",
|
||||
"article": "Art. 46 Abs. 1",
|
||||
"title": "Zusaetzliche Massnahmen"
|
||||
},
|
||||
{
|
||||
"norm": "EDPB",
|
||||
"article": "Recommendations 01/2020",
|
||||
"title": "Supplementary Measures"
|
||||
}
|
||||
],
|
||||
"sources": [
|
||||
{
|
||||
"type": "guidance",
|
||||
"ref": "EDPB Recommendations 01/2020"
|
||||
}
|
||||
],
|
||||
"category": "Technisch",
|
||||
"responsible": "IT-Sicherheit / Datenschutzbeauftragter",
|
||||
"priority": "hoch",
|
||||
"evidence": [
|
||||
{
|
||||
"name": "Nachweis zusaetzlicher Schutzmassnahmen",
|
||||
"required": true
|
||||
},
|
||||
"E2EE Dokumentation oder Pseudonymisierungskonzept"
|
||||
],
|
||||
"tom_control_ids": [
|
||||
"TOM.CRY.01",
|
||||
"TOM.GOV.01"
|
||||
],
|
||||
"valid_from": "2020-07-16",
|
||||
"version": "1.0"
|
||||
},
|
||||
{
|
||||
"id": "DSGVO-OBL-084",
|
||||
"title": "Informationspflicht bei Drittlanduebermittlung",
|
||||
"description": "Betroffene Personen muessen darueber informiert werden, dass ihre Daten in ein Drittland uebermittelt werden, einschliesslich der Angabe des Drittlands und der genutzten Garantien (Art. 13 Abs. 1 lit. f DSGVO).",
|
||||
"applies_when": "personal data transferred to third country",
|
||||
"applies_when_condition": {
|
||||
"all_of": [
|
||||
{
|
||||
"field": "data_protection.processes_personal_data",
|
||||
"operator": "EQUALS",
|
||||
"value": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"legal_basis": [
|
||||
{
|
||||
"norm": "DSGVO",
|
||||
"article": "Art. 13 Abs. 1 lit. f",
|
||||
"title": "Informationspflicht bei Drittlanduebermittlung"
|
||||
}
|
||||
],
|
||||
"category": "Organisatorisch",
|
||||
"responsible": "Datenschutzbeauftragter",
|
||||
"priority": "hoch",
|
||||
"evidence": [
|
||||
{
|
||||
"name": "Datenschutzerklaerung mit Drittland-Hinweis",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"tom_control_ids": [],
|
||||
"valid_from": "2018-05-25",
|
||||
"version": "1.0"
|
||||
}
|
||||
],
|
||||
"controls": [
|
||||
|
||||
5553
ai-compliance-sdk/policies/payment_controls_v1.json
Normal file
5553
ai-compliance-sdk/policies/payment_controls_v1.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -941,6 +941,676 @@ rules:
|
||||
gdpr_ref: "Art. 9(2)(h) DSGVO"
|
||||
rationale: "Gesundheitsdaten nur mit besonderen Schutzmaßnahmen"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# J. Drittlandtransfer / FISA 702
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
- id: R-FISA-001
|
||||
category: "J. Drittlandrisiko"
|
||||
title: "US-Cloud-Provider: FISA 702 Exposure"
|
||||
description: "Der Hosting-Provider unterliegt US-Recht (FISA 702, Cloud Act). Ein Zugriff durch US-Behoerden auf EU-Daten ist nicht ausschliessbar, unabhaengig vom Serverstandort."
|
||||
condition:
|
||||
field: "hosting.provider"
|
||||
operator: "in"
|
||||
value: ["aws", "azure", "google", "microsoft", "amazon", "openai", "anthropic", "oracle"]
|
||||
effect:
|
||||
risk_add: 20
|
||||
dsfa_recommended: true
|
||||
severity: WARN
|
||||
gdpr_ref: "Art. 44-49 DSGVO, Schrems II (C-311/18)"
|
||||
rationale: "FISA 702 erlaubt US-Behoerden Zugriff auf Daten von Nicht-US-Personen ohne richterlichen Beschluss. EU-Serverstandort schuetzt nicht."
|
||||
|
||||
- id: R-FISA-002
|
||||
category: "J. Drittlandrisiko"
|
||||
title: "Personenbezogene Daten bei US-Provider ohne E2EE"
|
||||
description: "Personenbezogene Daten werden bei einem US-Provider verarbeitet ohne dass eine Ende-zu-Ende-Verschluesselung mit kundenseitiger Schluesselhoheit vorliegt."
|
||||
condition:
|
||||
all_of:
|
||||
- field: "hosting.provider"
|
||||
operator: "in"
|
||||
value: ["aws", "azure", "google", "microsoft", "amazon", "openai", "anthropic", "oracle"]
|
||||
- field: "data_types.personal_data"
|
||||
operator: "equals"
|
||||
value: true
|
||||
effect:
|
||||
risk_add: 15
|
||||
controls_add: [C_ENCRYPTION]
|
||||
severity: WARN
|
||||
gdpr_ref: "Art. 32 DSGVO i.V.m. Art. 44 ff. DSGVO"
|
||||
rationale: "Ohne E2EE mit eigener Schluesselhoheit kann der Provider technisch auf Daten zugreifen und muss sie bei US-Anordnung herausgeben."
|
||||
|
||||
- id: R-FISA-003
|
||||
category: "J. Drittlandrisiko"
|
||||
title: "Besondere Datenkategorien bei US-Provider"
|
||||
description: "Besondere Kategorien personenbezogener Daten (Art. 9 DSGVO) werden bei einem US-Provider verarbeitet."
|
||||
condition:
|
||||
all_of:
|
||||
- field: "hosting.provider"
|
||||
operator: "in"
|
||||
value: ["aws", "azure", "google", "microsoft", "amazon", "openai", "anthropic", "oracle"]
|
||||
- field: "data_types.article_9_data"
|
||||
operator: "equals"
|
||||
value: true
|
||||
effect:
|
||||
risk_add: 25
|
||||
feasibility: CONDITIONAL
|
||||
dsfa_recommended: true
|
||||
severity: WARN
|
||||
gdpr_ref: "Art. 9 DSGVO i.V.m. Art. 49 DSGVO"
|
||||
rationale: "Besondere Kategorien bei FISA-exponierten Anbietern sind hochriskant. DSFA ist Pflicht."
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# K. Domain-spezifische Hochrisiko-Fragen (Annex III)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# HR / Recruiting (Annex III Nr. 4)
|
||||
- id: R-HR-001
|
||||
category: "K. HR Hochrisiko"
|
||||
title: "Automatisches Bewerber-Screening ohne Human Review"
|
||||
description: "KI sortiert Bewerber vor ohne dass ein Mensch jede Empfehlung tatsaechlich prueft"
|
||||
condition:
|
||||
all_of:
|
||||
- field: "hr_context.automated_screening"
|
||||
operator: "equals"
|
||||
value: true
|
||||
- field: "hr_context.human_review_enforced"
|
||||
operator: "equals"
|
||||
value: false
|
||||
effect:
|
||||
risk_add: 20
|
||||
feasibility: CONDITIONAL
|
||||
controls_add: [C_HUMAN_OVERSIGHT]
|
||||
severity: WARN
|
||||
gdpr_ref: "Art. 22 DSGVO + Annex III Nr. 4 AI Act"
|
||||
rationale: "Ohne echtes Human Review droht Art. 22 DSGVO Verstoss"
|
||||
|
||||
- id: R-HR-002
|
||||
category: "K. HR Hochrisiko"
|
||||
title: "Automatisierte Absagen — Art. 22 DSGVO Risiko"
|
||||
description: "KI generiert und versendet Absagen automatisch ohne menschliche Freigabe"
|
||||
condition:
|
||||
field: "hr_context.automated_rejection"
|
||||
operator: "equals"
|
||||
value: true
|
||||
effect:
|
||||
risk_add: 25
|
||||
feasibility: NO
|
||||
art22_risk: true
|
||||
severity: BLOCK
|
||||
gdpr_ref: "Art. 22 Abs. 1 DSGVO"
|
||||
rationale: "Vollautomatische Ablehnung = ausschliesslich automatisierte Entscheidung mit rechtlicher Wirkung"
|
||||
|
||||
- id: R-HR-003
|
||||
category: "K. HR Hochrisiko"
|
||||
title: "AGG-relevante Merkmale fuer KI erkennbar"
|
||||
description: "System kann Merkmale nach § 1 AGG erkennen (Name, Foto, Alter → Proxy-Diskriminierung)"
|
||||
condition:
|
||||
field: "hr_context.agg_categories_visible"
|
||||
operator: "equals"
|
||||
value: true
|
||||
effect:
|
||||
risk_add: 15
|
||||
controls_add: [C_BIAS_AUDIT]
|
||||
severity: WARN
|
||||
gdpr_ref: "§ 1, § 3 Abs. 2 AGG"
|
||||
rationale: "Proxy-Merkmale koennen indirekte Diskriminierung verursachen"
|
||||
|
||||
- id: R-HR-004
|
||||
category: "K. HR Hochrisiko"
|
||||
title: "Bewerber-Ranking ohne Bias-Audit"
|
||||
description: "KI erstellt Bewerber-Rankings ohne regelmaessige Bias-Pruefung"
|
||||
condition:
|
||||
all_of:
|
||||
- field: "hr_context.candidate_ranking"
|
||||
operator: "equals"
|
||||
value: true
|
||||
- field: "hr_context.bias_audits_done"
|
||||
operator: "equals"
|
||||
value: false
|
||||
effect:
|
||||
risk_add: 15
|
||||
controls_add: [C_BIAS_AUDIT]
|
||||
severity: WARN
|
||||
gdpr_ref: "§ 22 AGG (Beweislastumkehr)"
|
||||
rationale: "Ohne Bias-Audit keine Verteidigung bei AGG-Klage"
|
||||
|
||||
- id: R-HR-005
|
||||
category: "K. HR Hochrisiko"
|
||||
title: "KI-gestuetzte Mitarbeiterbewertung"
|
||||
description: "KI bewertet Mitarbeiterleistung (Performance Review, KPI-Tracking)"
|
||||
condition:
|
||||
field: "hr_context.performance_evaluation"
|
||||
operator: "equals"
|
||||
value: true
|
||||
effect:
|
||||
risk_add: 20
|
||||
severity: WARN
|
||||
gdpr_ref: "§ 87 Abs. 1 Nr. 6 BetrVG + § 94 BetrVG"
|
||||
rationale: "Leistungsbewertung durch KI ist mitbestimmungspflichtig und diskriminierungsriskant"
|
||||
|
||||
# Education (Annex III Nr. 3)
|
||||
- id: R-EDU-001
|
||||
category: "K. Bildung Hochrisiko"
|
||||
title: "KI beeinflusst Notenvergabe"
|
||||
description: "KI erstellt Notenvorschlaege oder beeinflusst Bewertungen"
|
||||
condition:
|
||||
field: "education_context.grade_influence"
|
||||
operator: "equals"
|
||||
value: true
|
||||
effect:
|
||||
risk_add: 20
|
||||
controls_add: [C_HUMAN_OVERSIGHT]
|
||||
dsfa_recommended: true
|
||||
severity: WARN
|
||||
gdpr_ref: "Annex III Nr. 3 AI Act"
|
||||
rationale: "Notenvergabe hat erhebliche Auswirkungen auf Bildungschancen"
|
||||
|
||||
- id: R-EDU-002
|
||||
category: "K. Bildung Hochrisiko"
|
||||
title: "Minderjaehrige betroffen ohne Lehrkraft-Review"
|
||||
description: "KI-System betrifft Minderjaehrige und Lehrkraft prueft nicht jedes Ergebnis"
|
||||
condition:
|
||||
all_of:
|
||||
- field: "education_context.minors_involved"
|
||||
operator: "equals"
|
||||
value: true
|
||||
- field: "education_context.teacher_review_required"
|
||||
operator: "equals"
|
||||
value: false
|
||||
effect:
|
||||
risk_add: 25
|
||||
feasibility: NO
|
||||
severity: BLOCK
|
||||
gdpr_ref: "Art. 24 EU-Grundrechtecharta + Annex III Nr. 3 AI Act"
|
||||
rationale: "KI-Entscheidungen ueber Minderjaehrige ohne Lehrkraft-Kontrolle sind unzulaessig"
|
||||
|
||||
- id: R-EDU-003
|
||||
category: "K. Bildung Hochrisiko"
|
||||
title: "KI steuert Zugang zu Bildungsangeboten"
|
||||
description: "KI beeinflusst Zulassung, Kursempfehlungen oder Einstufungen"
|
||||
condition:
|
||||
field: "education_context.student_selection"
|
||||
operator: "equals"
|
||||
value: true
|
||||
effect:
|
||||
risk_add: 20
|
||||
dsfa_recommended: true
|
||||
severity: WARN
|
||||
gdpr_ref: "Art. 14 EU-Grundrechtecharta (Recht auf Bildung)"
|
||||
rationale: "Zugangssteuerung zu Bildung ist hochrisiko nach AI Act"
|
||||
|
||||
# Healthcare (Annex III Nr. 5)
|
||||
- id: R-HC-001
|
||||
category: "K. Gesundheit Hochrisiko"
|
||||
title: "KI unterstuetzt Diagnosen"
|
||||
description: "KI erstellt Diagnosevorschlaege oder wertet Bildgebung aus"
|
||||
condition:
|
||||
field: "healthcare_context.diagnosis_support"
|
||||
operator: "equals"
|
||||
value: true
|
||||
effect:
|
||||
risk_add: 20
|
||||
dsfa_recommended: true
|
||||
controls_add: [C_HUMAN_OVERSIGHT]
|
||||
severity: WARN
|
||||
gdpr_ref: "Annex III Nr. 5 AI Act + MDR (EU) 2017/745"
|
||||
rationale: "Diagnoseunterstuetzung erfordert hoechste Genauigkeit und Human Oversight"
|
||||
|
||||
- id: R-HC-002
|
||||
category: "K. Gesundheit Hochrisiko"
|
||||
title: "Triage-Entscheidung durch KI"
|
||||
description: "KI priorisiert Patienten nach Dringlichkeit"
|
||||
condition:
|
||||
field: "healthcare_context.triage_decision"
|
||||
operator: "equals"
|
||||
value: true
|
||||
effect:
|
||||
risk_add: 30
|
||||
feasibility: CONDITIONAL
|
||||
controls_add: [C_HUMAN_OVERSIGHT]
|
||||
dsfa_recommended: true
|
||||
severity: WARN
|
||||
gdpr_ref: "Annex III Nr. 5 AI Act"
|
||||
rationale: "Lebenskritische Priorisierung erfordert maximale Sicherheit"
|
||||
|
||||
- id: R-HC-003
|
||||
category: "K. Gesundheit Hochrisiko"
|
||||
title: "Medizinprodukt ohne klinische Validierung"
|
||||
description: "System ist als Medizinprodukt eingestuft aber nicht klinisch validiert"
|
||||
condition:
|
||||
all_of:
|
||||
- field: "healthcare_context.medical_device"
|
||||
operator: "equals"
|
||||
value: true
|
||||
- field: "healthcare_context.clinical_validation"
|
||||
operator: "equals"
|
||||
value: false
|
||||
effect:
|
||||
risk_add: 30
|
||||
feasibility: NO
|
||||
severity: BLOCK
|
||||
gdpr_ref: "MDR (EU) 2017/745 Art. 61"
|
||||
rationale: "Medizinprodukte ohne klinische Validierung duerfen nicht in Verkehr gebracht werden"
|
||||
|
||||
- id: R-HC-004
|
||||
category: "K. Gesundheit Hochrisiko"
|
||||
title: "Gesundheitsdaten ohne besondere Schutzmassnahmen"
|
||||
description: "Gesundheitsdaten (Art. 9 DSGVO) werden verarbeitet"
|
||||
condition:
|
||||
field: "healthcare_context.patient_data_processed"
|
||||
operator: "equals"
|
||||
value: true
|
||||
effect:
|
||||
risk_add: 15
|
||||
dsfa_recommended: true
|
||||
controls_add: [C_DSFA]
|
||||
severity: WARN
|
||||
gdpr_ref: "Art. 9 DSGVO"
|
||||
rationale: "Gesundheitsdaten sind besondere Kategorien mit erhoehtem Schutzbedarf"
|
||||
|
||||
# Legal / Justice (Annex III Nr. 8)
|
||||
- id: R-LEG-001
|
||||
category: "K. Legal Hochrisiko"
|
||||
title: "KI gibt Rechtsberatung"
|
||||
description: "KI generiert rechtliche Empfehlungen oder Einschaetzungen"
|
||||
condition: { field: "legal_context.legal_advice", operator: "equals", value: true }
|
||||
effect: { risk_add: 15, controls_add: [C_HUMAN_OVERSIGHT] }
|
||||
severity: WARN
|
||||
gdpr_ref: "Annex III Nr. 8 AI Act"
|
||||
rationale: "Rechtsberatung durch KI kann Zugang zur Justiz beeintraechtigen"
|
||||
|
||||
- id: R-LEG-002
|
||||
category: "K. Legal Hochrisiko"
|
||||
title: "KI prognostiziert Gerichtsurteile"
|
||||
description: "System erstellt Prognosen ueber Verfahrensausgaenge"
|
||||
condition: { field: "legal_context.court_prediction", operator: "equals", value: true }
|
||||
effect: { risk_add: 20, dsfa_recommended: true }
|
||||
severity: WARN
|
||||
rationale: "Urteilsprognosen koennen rechtliches Verhalten verzerren"
|
||||
|
||||
- id: R-LEG-003
|
||||
category: "K. Legal Hochrisiko"
|
||||
title: "Mandantengeheimnis bei KI-Verarbeitung"
|
||||
description: "Vertrauliche Mandantendaten werden durch KI verarbeitet"
|
||||
condition: { field: "legal_context.client_confidential", operator: "equals", value: true }
|
||||
effect: { risk_add: 15, controls_add: [C_ENCRYPTION] }
|
||||
severity: WARN
|
||||
rationale: "Mandantengeheimnis erfordert besonderen Schutz (§ 203 StGB)"
|
||||
|
||||
# Public Sector (Art. 27 FRIA)
|
||||
- id: R-PUB-001
|
||||
category: "K. Oeffentlicher Sektor"
|
||||
title: "KI in Verwaltungsentscheidungen"
|
||||
description: "KI beeinflusst Verwaltungsakte oder Bescheide"
|
||||
condition: { field: "public_sector_context.admin_decision", operator: "equals", value: true }
|
||||
effect: { risk_add: 25, dsfa_recommended: true, controls_add: [C_FRIA, C_HUMAN_OVERSIGHT] }
|
||||
severity: WARN
|
||||
rationale: "Verwaltungsentscheidungen erfordern FRIA (Art. 27 AI Act)"
|
||||
|
||||
- id: R-PUB-002
|
||||
category: "K. Oeffentlicher Sektor"
|
||||
title: "KI verteilt oeffentliche Leistungen"
|
||||
description: "KI entscheidet ueber Zuteilung von Sozialleistungen oder Foerderung"
|
||||
condition: { field: "public_sector_context.benefit_allocation", operator: "equals", value: true }
|
||||
effect: { risk_add: 25, feasibility: CONDITIONAL }
|
||||
severity: WARN
|
||||
rationale: "Leistungszuteilung betrifft Grundrecht auf soziale Sicherheit"
|
||||
|
||||
- id: R-PUB-003
|
||||
category: "K. Oeffentlicher Sektor"
|
||||
title: "Fehlende Transparenz gegenueber Buergern"
|
||||
condition:
|
||||
all_of:
|
||||
- field: "public_sector_context.citizen_service"
|
||||
operator: "equals"
|
||||
value: true
|
||||
- field: "public_sector_context.transparency_ensured"
|
||||
operator: "equals"
|
||||
value: false
|
||||
effect: { risk_add: 15, controls_add: [C_TRANSPARENCY] }
|
||||
severity: WARN
|
||||
rationale: "Oeffentliche Stellen haben erhoehte Transparenzpflicht"
|
||||
|
||||
# Critical Infrastructure (NIS2 + Annex III Nr. 2)
|
||||
- id: R-CRIT-001
|
||||
category: "K. Kritische Infrastruktur"
|
||||
title: "Sicherheitskritische KI-Steuerung ohne Redundanz"
|
||||
condition:
|
||||
all_of:
|
||||
- field: "critical_infra_context.safety_critical"
|
||||
operator: "equals"
|
||||
value: true
|
||||
- field: "critical_infra_context.redundancy_exists"
|
||||
operator: "equals"
|
||||
value: false
|
||||
effect: { risk_add: 30, feasibility: NO }
|
||||
severity: BLOCK
|
||||
rationale: "Sicherheitskritische Steuerung ohne Redundanz ist unzulaessig"
|
||||
|
||||
- id: R-CRIT-002
|
||||
category: "K. Kritische Infrastruktur"
|
||||
title: "KI steuert Netz-/Infrastruktur"
|
||||
condition: { field: "critical_infra_context.grid_control", operator: "equals", value: true }
|
||||
effect: { risk_add: 20, controls_add: [C_INCIDENT_RESPONSE, C_HUMAN_OVERSIGHT] }
|
||||
severity: WARN
|
||||
rationale: "Netzsteuerung durch KI erfordert NIS2-konforme Absicherung"
|
||||
|
||||
# Automotive / Aerospace
|
||||
- id: R-AUTO-001
|
||||
category: "K. Automotive Hochrisiko"
|
||||
title: "Autonomes Fahren / ADAS"
|
||||
condition: { field: "automotive_context.autonomous_driving", operator: "equals", value: true }
|
||||
effect: { risk_add: 30, controls_add: [C_HUMAN_OVERSIGHT, C_FRIA] }
|
||||
severity: WARN
|
||||
rationale: "Autonomes Fahren ist sicherheitskritisch und hochreguliert"
|
||||
|
||||
- id: R-AUTO-002
|
||||
category: "K. Automotive Hochrisiko"
|
||||
title: "Sicherheitsrelevant ohne Functional Safety"
|
||||
condition:
|
||||
all_of:
|
||||
- field: "automotive_context.safety_relevant"
|
||||
operator: "equals"
|
||||
value: true
|
||||
- field: "automotive_context.functional_safety"
|
||||
operator: "equals"
|
||||
value: false
|
||||
effect: { risk_add: 25, feasibility: CONDITIONAL }
|
||||
severity: WARN
|
||||
rationale: "Sicherheitsrelevante Systeme erfordern ISO 26262 Konformitaet"
|
||||
|
||||
# Retail / E-Commerce
|
||||
- id: R-RET-001
|
||||
category: "K. Retail"
|
||||
title: "Personalisierte Preise durch KI"
|
||||
condition: { field: "retail_context.pricing_personalized", operator: "equals", value: true }
|
||||
effect: { risk_add: 15, controls_add: [C_TRANSPARENCY] }
|
||||
severity: WARN
|
||||
rationale: "Personalisierte Preise koennen Verbraucher benachteiligen (DSA Art. 25)"
|
||||
|
||||
- id: R-RET-002
|
||||
category: "K. Retail"
|
||||
title: "Bonitaetspruefung bei Kauf"
|
||||
condition: { field: "retail_context.credit_scoring", operator: "equals", value: true }
|
||||
effect: { risk_add: 20, dsfa_recommended: true, art22_risk: true }
|
||||
severity: WARN
|
||||
rationale: "Kredit-Scoring ist Annex III Nr. 5 AI Act (Zugang zu Diensten)"
|
||||
|
||||
- id: R-RET-003
|
||||
category: "K. Retail"
|
||||
title: "Dark Patterns moeglich"
|
||||
condition: { field: "retail_context.dark_patterns", operator: "equals", value: true }
|
||||
effect: { risk_add: 15 }
|
||||
severity: WARN
|
||||
rationale: "Manipulative UI-Muster verstossen gegen DSA und Verbraucherrecht"
|
||||
|
||||
# IT / Cybersecurity / Telecom
|
||||
- id: R-ITS-001
|
||||
category: "K. IT-Sicherheit"
|
||||
title: "KI-gestuetzte Mitarbeiterueberwachung"
|
||||
condition: { field: "it_security_context.employee_surveillance", operator: "equals", value: true }
|
||||
effect: { risk_add: 20, dsfa_recommended: true }
|
||||
severity: WARN
|
||||
rationale: "Mitarbeiterueberwachung ist §87 BetrVG + DSGVO relevant"
|
||||
|
||||
- id: R-ITS-002
|
||||
category: "K. IT-Sicherheit"
|
||||
title: "Umfangreiche Log-Speicherung"
|
||||
condition: { field: "it_security_context.data_retention_logs", operator: "equals", value: true }
|
||||
effect: { risk_add: 10, controls_add: [C_DATA_MINIMIZATION] }
|
||||
severity: INFO
|
||||
rationale: "Datenminimierung beachten auch bei Security-Logs"
|
||||
|
||||
# Logistics
|
||||
- id: R-LOG-001
|
||||
category: "K. Logistik"
|
||||
title: "Fahrer-/Kurier-Tracking"
|
||||
condition: { field: "logistics_context.driver_tracking", operator: "equals", value: true }
|
||||
effect: { risk_add: 20 }
|
||||
severity: WARN
|
||||
rationale: "GPS-Tracking ist Verhaltenskontrolle (§87 BetrVG)"
|
||||
|
||||
- id: R-LOG-002
|
||||
category: "K. Logistik"
|
||||
title: "Leistungsbewertung Lagerarbeiter"
|
||||
condition: { field: "logistics_context.workload_scoring", operator: "equals", value: true }
|
||||
effect: { risk_add: 20, art22_risk: true }
|
||||
severity: WARN
|
||||
rationale: "Leistungs-Scoring ist Annex III Nr. 4 (Employment)"
|
||||
|
||||
# Construction / Real Estate
|
||||
- id: R-CON-001
|
||||
category: "K. Bau/Immobilien"
|
||||
title: "KI-gestuetzte Mieterauswahl"
|
||||
condition: { field: "construction_context.tenant_screening", operator: "equals", value: true }
|
||||
effect: { risk_add: 20, dsfa_recommended: true }
|
||||
severity: WARN
|
||||
rationale: "Mieterauswahl betrifft Zugang zu Wohnraum (Grundrecht)"
|
||||
|
||||
- id: R-CON-002
|
||||
category: "K. Bau/Immobilien"
|
||||
title: "KI-Arbeitsschutzueberwachung"
|
||||
condition: { field: "construction_context.worker_safety", operator: "equals", value: true }
|
||||
effect: { risk_add: 15 }
|
||||
severity: WARN
|
||||
rationale: "Arbeitsschutzueberwachung kann Verhaltenskontrolle sein"
|
||||
|
||||
# Marketing / Media
|
||||
- id: R-MKT-001
|
||||
category: "K. Marketing/Medien"
|
||||
title: "Deepfake-Inhalte ohne Kennzeichnung"
|
||||
condition:
|
||||
all_of:
|
||||
- field: "marketing_context.deepfake_content"
|
||||
operator: "equals"
|
||||
value: true
|
||||
- field: "marketing_context.ai_content_labeled"
|
||||
operator: "equals"
|
||||
value: false
|
||||
effect: { risk_add: 20, feasibility: NO }
|
||||
severity: BLOCK
|
||||
rationale: "Art. 50 Abs. 4 AI Act: Deepfakes muessen gekennzeichnet werden"
|
||||
|
||||
- id: R-MKT-002
|
||||
category: "K. Marketing/Medien"
|
||||
title: "Minderjaehrige als Zielgruppe"
|
||||
condition: { field: "marketing_context.minors_targeted", operator: "equals", value: true }
|
||||
effect: { risk_add: 20, controls_add: [C_DSFA] }
|
||||
severity: WARN
|
||||
rationale: "Besonderer Schutz Minderjaehriger (DSA + DSGVO)"
|
||||
|
||||
- id: R-MKT-003
|
||||
category: "K. Marketing/Medien"
|
||||
title: "Verhaltensbasiertes Targeting"
|
||||
condition: { field: "marketing_context.behavioral_targeting", operator: "equals", value: true }
|
||||
effect: { risk_add: 15, dsfa_recommended: true }
|
||||
severity: WARN
|
||||
rationale: "Behavioral Targeting ist Profiling (Art. 22 DSGVO)"
|
||||
|
||||
# Manufacturing / CE
|
||||
- id: R-MFG-001
|
||||
category: "K. Fertigung"
|
||||
title: "KI in Maschinensicherheit ohne Validierung"
|
||||
condition:
|
||||
all_of:
|
||||
- field: "manufacturing_context.machine_safety"
|
||||
operator: "equals"
|
||||
value: true
|
||||
- field: "manufacturing_context.safety_validated"
|
||||
operator: "equals"
|
||||
value: false
|
||||
effect: { risk_add: 30, feasibility: NO }
|
||||
severity: BLOCK
|
||||
rationale: "Maschinenverordnung (EU) 2023/1230 erfordert Sicherheitsvalidierung"
|
||||
|
||||
- id: R-MFG-002
|
||||
category: "K. Fertigung"
|
||||
title: "CE-Kennzeichnung erforderlich"
|
||||
condition: { field: "manufacturing_context.ce_marking_required", operator: "equals", value: true }
|
||||
effect: { risk_add: 15, controls_add: [C_CE_CONFORMITY] }
|
||||
severity: WARN
|
||||
rationale: "CE-Kennzeichnung ist Pflicht fuer Maschinenprodukte mit KI"
|
||||
|
||||
# Agriculture
|
||||
- id: R-AGR-001
|
||||
category: "K. Landwirtschaft"
|
||||
title: "KI steuert Pestizideinsatz"
|
||||
condition: { field: "agriculture_context.pesticide_ai", operator: "equals", value: true }
|
||||
effect: { risk_add: 15 }
|
||||
severity: WARN
|
||||
rationale: "Umwelt- und Gesundheitsrisiken bei KI-gesteuertem Pflanzenschutz"
|
||||
|
||||
- id: R-AGR-002
|
||||
category: "K. Landwirtschaft"
|
||||
title: "KI beeinflusst Tierhaltung"
|
||||
condition: { field: "agriculture_context.animal_welfare", operator: "equals", value: true }
|
||||
effect: { risk_add: 10 }
|
||||
severity: INFO
|
||||
rationale: "Tierschutzrelevanz bei automatisierter Haltungsentscheidung"
|
||||
|
||||
# Social Services
|
||||
- id: R-SOC-001
|
||||
category: "K. Soziales"
|
||||
title: "KI trifft Leistungsentscheidungen fuer schutzbeduerftiger Gruppen"
|
||||
condition:
|
||||
all_of:
|
||||
- field: "social_services_context.vulnerable_groups"
|
||||
operator: "equals"
|
||||
value: true
|
||||
- field: "social_services_context.benefit_decision"
|
||||
operator: "equals"
|
||||
value: true
|
||||
effect: { risk_add: 25, dsfa_recommended: true, controls_add: [C_FRIA, C_HUMAN_OVERSIGHT] }
|
||||
severity: WARN
|
||||
rationale: "Leistungsentscheidungen fuer Schutzbeduerftiger erfordern FRIA"
|
||||
|
||||
- id: R-SOC-002
|
||||
category: "K. Soziales"
|
||||
title: "KI in Fallmanagement"
|
||||
condition: { field: "social_services_context.case_management", operator: "equals", value: true }
|
||||
effect: { risk_add: 15 }
|
||||
severity: WARN
|
||||
rationale: "Fallmanagement betrifft Grundrechte der Betroffenen"
|
||||
|
||||
# Hospitality / Tourism
|
||||
- id: R-HOS-001
|
||||
category: "K. Tourismus"
|
||||
title: "Dynamische Preisgestaltung"
|
||||
condition: { field: "hospitality_context.dynamic_pricing", operator: "equals", value: true }
|
||||
effect: { risk_add: 10, controls_add: [C_TRANSPARENCY] }
|
||||
severity: INFO
|
||||
rationale: "Personalisierte Preise erfordern Transparenz"
|
||||
|
||||
- id: R-HOS-002
|
||||
category: "K. Tourismus"
|
||||
title: "KI manipuliert Bewertungen"
|
||||
condition: { field: "hospitality_context.review_manipulation", operator: "equals", value: true }
|
||||
effect: { risk_add: 20, feasibility: NO }
|
||||
severity: BLOCK
|
||||
rationale: "Bewertungsmanipulation verstoesst gegen UWG und DSA"
|
||||
|
||||
# Insurance
|
||||
- id: R-INS-001
|
||||
category: "K. Versicherung"
|
||||
title: "KI-gestuetzte Praemienberechnung"
|
||||
condition: { field: "insurance_context.premium_calculation", operator: "equals", value: true }
|
||||
effect: { risk_add: 20, dsfa_recommended: true }
|
||||
severity: WARN
|
||||
rationale: "Individuelle Praemien koennen diskriminierend wirken (AGG, Annex III Nr. 5)"
|
||||
|
||||
- id: R-INS-002
|
||||
category: "K. Versicherung"
|
||||
title: "Automatisierte Schadenbearbeitung"
|
||||
condition: { field: "insurance_context.claims_automation", operator: "equals", value: true }
|
||||
effect: { risk_add: 15, art22_risk: true }
|
||||
severity: WARN
|
||||
rationale: "Automatische Schadensablehnung kann Art. 22 DSGVO ausloesen"
|
||||
|
||||
# Investment
|
||||
- id: R-INV-001
|
||||
category: "K. Investment"
|
||||
title: "Algorithmischer Handel"
|
||||
condition: { field: "investment_context.algo_trading", operator: "equals", value: true }
|
||||
effect: { risk_add: 15 }
|
||||
severity: WARN
|
||||
rationale: "MiFID II Anforderungen an algorithmischen Handel"
|
||||
|
||||
- id: R-INV-002
|
||||
category: "K. Investment"
|
||||
title: "KI-gestuetzte Anlageberatung (Robo Advisor)"
|
||||
condition: { field: "investment_context.robo_advisor", operator: "equals", value: true }
|
||||
effect: { risk_add: 20, controls_add: [C_HUMAN_OVERSIGHT, C_TRANSPARENCY] }
|
||||
severity: WARN
|
||||
rationale: "Anlageberatung ist reguliert (WpHG, MiFID II) — Haftungsrisiko"
|
||||
|
||||
# Defense
|
||||
- id: R-DEF-001
|
||||
category: "K. Verteidigung"
|
||||
title: "Dual-Use KI-Technologie"
|
||||
condition: { field: "defense_context.dual_use", operator: "equals", value: true }
|
||||
effect: { risk_add: 25 }
|
||||
severity: WARN
|
||||
rationale: "Dual-Use Technologie unterliegt Exportkontrolle (EU VO 2021/821)"
|
||||
|
||||
- id: R-DEF-002
|
||||
category: "K. Verteidigung"
|
||||
title: "Verschlusssachen in KI verarbeitet"
|
||||
condition: { field: "defense_context.classified_data", operator: "equals", value: true }
|
||||
effect: { risk_add: 20, controls_add: [C_ENCRYPTION] }
|
||||
severity: WARN
|
||||
rationale: "VS-NfD und hoeher erfordert besondere Schutzmassnahmen"
|
||||
|
||||
# Supply Chain (LkSG)
|
||||
- id: R-SCH-001
|
||||
category: "K. Lieferkette"
|
||||
title: "KI-Menschenrechtspruefung in Lieferkette"
|
||||
condition: { field: "supply_chain_context.human_rights_check", operator: "equals", value: true }
|
||||
effect: { risk_add: 10 }
|
||||
severity: INFO
|
||||
rationale: "LkSG-relevante KI-Analyse — Bias bei Laenderrisiko-Bewertung beachten"
|
||||
|
||||
- id: R-SCH-002
|
||||
category: "K. Lieferkette"
|
||||
title: "KI ueberwacht Lieferanten"
|
||||
condition: { field: "supply_chain_context.supplier_monitoring", operator: "equals", value: true }
|
||||
effect: { risk_add: 10 }
|
||||
severity: INFO
|
||||
rationale: "Lieferantenbewertung durch KI kann indirekt Personen betreffen"
|
||||
|
||||
# Facility Management
|
||||
- id: R-FAC-001
|
||||
category: "K. Facility"
|
||||
title: "KI-Zutrittskontrolle"
|
||||
condition: { field: "facility_context.access_control_ai", operator: "equals", value: true }
|
||||
effect: { risk_add: 15, dsfa_recommended: true }
|
||||
severity: WARN
|
||||
rationale: "Biometrische oder verhaltensbasierte Zutrittskontrolle ist DSGVO-relevant"
|
||||
|
||||
- id: R-FAC-002
|
||||
category: "K. Facility"
|
||||
title: "Belegungsueberwachung"
|
||||
condition: { field: "facility_context.occupancy_tracking", operator: "equals", value: true }
|
||||
effect: { risk_add: 10 }
|
||||
severity: INFO
|
||||
rationale: "Belegungsdaten koennen Rueckschluesse auf Verhalten erlauben"
|
||||
|
||||
# Sports
|
||||
- id: R-SPO-001
|
||||
category: "K. Sport"
|
||||
title: "Athleten-Performance-Tracking"
|
||||
condition: { field: "sports_context.athlete_tracking", operator: "equals", value: true }
|
||||
effect: { risk_add: 15 }
|
||||
severity: WARN
|
||||
rationale: "Leistungsdaten von Athleten sind besonders schuetzenswert"
|
||||
|
||||
- id: R-SPO-002
|
||||
category: "K. Sport"
|
||||
title: "Fan-/Zuschauer-Profilbildung"
|
||||
condition: { field: "sports_context.fan_profiling", operator: "equals", value: true }
|
||||
effect: { risk_add: 15, dsfa_recommended: true }
|
||||
severity: WARN
|
||||
rationale: "Massen-Profiling bei Sportevents erfordert DSFA"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# G. Aggregation & Ergebnis
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user