28 Commits

Author SHA1 Message Date
Benjamin Admin
062e827801 feat: Sidebar — KI-Compliance Links + Payment Info-Box
Sidebar: Neue Sektion "KI-Compliance" mit 4 Links:
- Use Case Erfassung (advisory-board)
- Use Cases (use-cases)
- AI Act (ai-act)
- EU Registrierung (ai-registration)

Payment: Info-Box mit 3-Spalten Erklaerung (Controls → Assessment → Ausschreibung)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 18:21:35 +02:00
Benjamin Admin
f404226d6e fix: Payment page ternary syntax for 3-tab layout 2026-04-13 17:40:46 +02:00
Benjamin Admin
8dfab4ba14 feat: Payment Compliance Pack — Semgrep + CodeQL + State Machine + Schema
Ausfuehrbares Pruefpaket fuer Payment-Terminal-Systeme:

1. Semgrep-Regeln (25 Regeln in 5 Dateien):
   - Logging: Sensitive Daten, Tokens, Debug-Flags
   - Crypto: MD5/SHA1/DES/ECB, Hardcoded Secrets, Weak Random, TLS
   - API: Debug-Routes, Exception Leaks, IDOR, Input Validation
   - Config: Test-Endpoints, CORS, Cookies, Retry
   - Data: Telemetrie, Cache, Export, Queue, Testdaten

2. CodeQL Query-Specs (5 Briefings):
   - Sensitive Data → Logs
   - Sensitive Data → HTTP Response
   - Tenant Context Loss
   - Sensitive Data → Telemetry
   - Cache/Export Leak

3. State-Machine-Tests (10 Testfaelle):
   - 11 Zustaende, 15 Events, 8 Invarianten
   - Duplicate Response, Timeout+Late Success, Decline
   - Invalid Reversal, Cancel, Backend Timeout
   - Parallel Reversal, Unknown Response, Reconnect
   - Late Response after Cancel

4. Finding Schema (JSON Schema):
   - Einheitliches Format fuer alle Engines
   - control_id, engine, status, confidence, evidence, verdict_text

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 14:59:49 +02:00
Benjamin Admin
5c1a514b52 feat: Payment Controls auf 445 erweitert — ZVT/OPI Protokoll komplett
+37 Controls in 8 neuen Domaenen:
- TERMSYNC (2): Sync-Entscheidungen, Divergenzpruefung
- ZVT-CMD (5): Kommandoreihenfolge, Parameter, Antwortverarbeitung
- ZVT-RT (5): Timeouts, Retry, Backoff, Abbruch-Markierung
- ZVT-STATE (5): State Machine, Exit-Pfade, Recovery
- ZVT-COM (5): Nachrichtenlaenge, Checksummen, Encoding
- ZVT-REV (5): Reversal, Storno, Mehrfachschutz
- ZVT-RESP (5): Response-Codes, Fehlerinterpretation
- ZVT-SESSION (5): Session-Lifecycle, Timeout, Parallelitaet

445 Controls total, 43 Domaenen

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 12:57:05 +02:00
Benjamin Admin
e091bbc855 feat: ZVT/OPI/Terminal Controls — 408 total (9 neue Domaenen)
+90 Controls fuer Terminal-Protokollverhalten:
- ZVTCORE (10): Rahmenstruktur, Parser, Feldvalidierung
- ZVTFLOW (10): Kommandosequenzen, Zustandsuebergaenge
- ZVTERROR (10): Fehlercodes, Klassifikation, Eskalation
- ZVTTIME (10): Timeouts, Retry, Busy-States
- OPICORE (10): Nachrichtenstruktur, Schema, Parser
- OPIFLOW (10): Ablaufsteuerung, Korrelation, Recovery
- PROTOINT (10): Protokollkonverter, Mapping, Adapter
- TERMSTATE (10): Terminalzustaende, Reconnect, Safe States
- TERMREC (10): Belegdaten, Validierung, Datenschutz

408 Controls total (war 318), 35 Domaenen

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 12:45:10 +02:00
Benjamin Admin
ff4c359d46 feat: Payment Controls auf 318 erweitert (26 Domaenen)
+100 Controls in 10 neuen Domaenen:
- BUILD (10): Pipeline-Sicherheit, Artefakt-Integritaet, Abhaengigkeiten
- DEPLOY (10): Release-Management, Rollback, Umgebungstrennung
- QUEUE (10): Warteschlangen, Dead-Letter, Idempotenz, Reihenfolge
- TENANT (10): Mandantentrennung, Cross-Tenant-Schutz, Cache-Isolation
- TELEMETRY (10): Metriken, Tracing, Datenmaskierung in Observability
- CONFIG (10): Defaults, Validierung, Feature Flags, Laufzeitaenderungen
- NETWORK (10): Segmentierung, Firewall, TLS, Egress-Kontrolle
- STORAGE (10): Persistenz, Backup, Schema-Integritaet, Zugriffskontrolle
- MONITOR (10): Alarmierung, Heartbeats, Schwellwerte, Incident Detection
- OPS (10): Betriebsprozesse, Runbooks, Wartung, Recovery

318 Controls total (war 218)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 12:29:30 +02:00
Benjamin Admin
f169b13dbf feat: Payment Controls auf 218 erweitert (16 Domaenen)
Neue Domaenen hinzugefuegt:
- AUTH (20): Authentifizierung, MFA, Privilege Escalation, Cross-Tenant
- SESSION (10): Token, Cookies, Fixation, Timeout, SameSite
- KEYMGMT (10): Rotation, Provisioning, Revocation, Lifecycle
- DEVICE (15): Geraeteidentitaet, Tamper, Provisioning, Safe States
- TRANS (10): State Machine, Idempotenz, Race Conditions, Stornierung
- DATA (8): Minimierung, Maskierung, Telemetrie, Testdaten
Erweitert: CRYPTO +5 (ECB, IV-Reuse, Timing, Fallbacks), ERR +5, REP +5

218 Controls total (war 130)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 09:54:51 +02:00
Benjamin Admin
42d0c7b1fc feat: Payment Compliance in Sidebar Navigation
Neuer Sidebar-Eintrag "Payment / Terminal" mit Kreditkarten-Icon
zwischen CE/IACE und Zusatzmodule.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 09:43:50 +02:00
Benjamin Admin
4fcb842a92 feat: Tender-Analyse Pipeline — Upload, Extraction, Control-Matching
Phase 3 des Payment Compliance Moduls:
1. Backend: Tender Upload + LLM Requirement Extraction + Control Matching
   - DB Migration 025 (tender_analyses Tabelle)
   - TenderHandlers: Upload, Extract, Match, List, Get (5 Endpoints)
   - LLM-Extraktion via Anthropic API mit Keyword-Fallback
   - Control-Matching mit Domain-Bonus + Keyword-Overlap Relevance
2. Frontend: Dritter Tab "Ausschreibung" in /sdk/payment-compliance
   - PDF/TXT/Word Upload mit Drag-Area
   - Automatische Analyse-Pipeline (Upload → Extract → Match)
   - Ergebnis-Dashboard: Abgedeckt/Teilweise/Luecken
   - Requirement-by-Requirement Matching mit Control-IDs + Relevanz%
   - Gap-Beschreibung fuer nicht-gematchte Requirements
   - Analyse-Historie mit Klick-to-Detail

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 09:35:46 +02:00
Benjamin Admin
38d3d24121 feat: Payment Terminal Compliance Modul — Phase 1+2
1. Control-Bibliothek: 130 Controls in 10 Domaenen (payment_controls_v1.json)
   - PAY (20): Transaction Flow, Idempotenz, State Machine
   - LOG (15): Audit Trail, PAN-Maskierung, Event-Typen
   - CRYPTO (15): Secrets, HSM, P2PE, TLS
   - API (15): Auth, RBAC, Rate Limiting, Injection
   - TERM (15): ZVT/OPI, Heartbeat, Offline-Queue
   - FW (10): Firmware Signing, Secure Boot, Tamper Detection
   - REP (10): Reconciliation, Tagesabschluss, GoBD
   - ACC (10): MFA, Session, Least Privilege
   - ERR (10): Recovery, Circuit Breaker, Offline-Modus
   - BLD (10): CI/CD, SBOM, Container Scanning
2. Backend: DB Migration 024, Go Handler (5 Endpoints), Routes
3. Frontend: /sdk/payment-compliance mit Control-Browser + Assessment-Wizard

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 07:51:59 +02:00
Benjamin Admin
dd64e33e88 docs: SDK-Flow + Wiki — EU Registration Step + 4 Domain-Artikel
1. SDK-Flow: Neuer Step "EU AI Database Registrierung" (seq 350, CP-REG)
2. Wiki: 4 Domain-Compliance-Artikel (Recruiting, Bildung, Gesundheit, Finance)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 07:13:17 +02:00
Benjamin Admin
2f8269d115 test: Domain-Context Tests — 22 Tests (HR, Edu, HC, CritInfra, Marketing, Mfg, AGG)
BLOCK-Tests: AutomatedRejection, MinorsWithoutTeacher, MDRUnvalidated,
             SafetyCriticalNoRedundancy, DeepfakeUnlabeled, ManufacturingUnvalidated,
             ReviewManipulation
Positive Tests: HumanReview OK, TeacherReview OK, DeepfakeLabeled OK
Risk Tests: AGG visible, Triage high risk
Loader Tests: AGG + AI Act obligations count, applicability
Resolver Tests: HRContext, NilContext, HealthcareContext
Meta: TotalObligationsCount, DomainConstants

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 06:59:11 +02:00
Benjamin Admin
532febe35c fix: Build-Fehler — LegalContext Namenskollision + Registration Handler
- LegalContext → LegalDomainContext (Kollision mit legal_rag.go LegalContext)
- ExplainResponse.LegalContext bleibt unveraendert (RAG-Typ)
- Registration Handler: Intake ist struct, kein []byte
- Unbenutzten json Import entfernt

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 23:57:00 +02:00
Benjamin Admin
0a0863f31c feat: Letzte 3 Domains abgedeckt — Finance/Banking + General (100%)
- Finance/Banking: Kredit-Scoring, AML/KYC, automatisierte Entscheidungen, Kunden-Profiling
- General: Universelle KI-Governance (Personenbezug, Automatisierung, sensible Daten)

Domains mit Fragen: 27 Gruppen fuer alle 54 Domains (100% Coverage)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 23:12:00 +02:00
Benjamin Admin
d892ad161f feat: Domain-Fragen fuer 10 weitere Domains (24 von 39 total, 62%)
10 neue Context-Structs + Field-Resolver + 22 YAML-Regeln + Frontend:
- Agriculture: Pestizid-KI, Tierwohl, Umweltdaten
- Social Services: Schutzbeduerftiger, Leistungszuteilung, Fallmanagement
- Hospitality: Gaeste-Profiling, dynamische Preise, Bewertungsmanipulation=BLOCK
- Insurance: Praemien, Schadensautomation, Betrugserkennung
- Investment: Algo-Trading, Robo Advisor (MiFID II)
- Defense: Dual-Use, Exportkontrolle, Verschlusssachen
- Supply Chain: Lieferantenueberwachung, Menschenrechte (LkSG)
- Facility: Zutrittskontrolle, Belegung, Energie
- Sports: Athleten-Tracking, Fan-Profiling

Domains mit Fragen: 24 von 39 (62%)
YAML-Regeln total: ~66
Neue BLOCKs: Bewertungsmanipulation (UWG/DSA)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 23:04:35 +02:00
Benjamin Admin
17153ccbe8 feat: Domain-Fragen fuer 10 weitere Domains (14 total)
10 neue Context-Structs + Field-Resolver + ~30 YAML-Regeln + Frontend:
- Legal/Justice: Rechtsberatung, Urteilsprognose, Mandantengeheimnis
- Public Sector: Verwaltungsentscheidungen, Leistungsverteilung, FRIA
- Critical Infra: Netzsteuerung, Sicherheitskritisch, Redundanz
- Automotive: Autonomes Fahren, ADAS, ISO 26262
- Retail/E-Commerce: Preise, Scoring, Dark Patterns
- IT/Cybersecurity: Surveillance, Threat Detection, Log-Retention
- Logistics: Fahrer-Tracking, Workload-Scoring
- Construction: Mieterauswahl, Arbeitsschutz
- Marketing/Media: Deepfakes=BLOCK, Minderjaehrige, Targeting
- Manufacturing: Maschinensicherheit=BLOCK, CE-Kennzeichnung

Domains mit Fragen: 14 von 39 (36%)
YAML-Regeln total: ~44 (14 vorher + 30 neu)
BLOCK-Regeln: Deepfakes ungekennzeichnet, Maschinensicherheit unvalidiert,
              Kritische Infra ohne Redundanz

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 22:50:26 +02:00
Benjamin Admin
352d7112c9 feat: Domain YAML-Regeln (14 Regeln) + Field-Resolver fuer HR/Edu/HC
1. 14 neue YAML-Regeln in Kategorie K (Domain-Hochrisiko):
   - HR: 5 Regeln (Screening, Absagen=BLOCK, AGG, Bias, Performance)
   - Education: 3 Regeln (Noten, Minderjaehrige=BLOCK, Zugangssteuerung)
   - Healthcare: 4 Regeln (Diagnose, Triage, MDR=BLOCK, Gesundheitsdaten)
2. Field-Resolver: getHRContextValue(), getEducationContextValue(), getHealthcareContextValue()

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 22:35:48 +02:00
Benjamin Admin
0957254547 feat: Domain-spezifische UCCA-Fragen (HR, Education, Healthcare) + AGG-Modul
1. Domain-Context Structs: HRContext (7 Felder), EducationContext (6), HealthcareContext (6)
   — nach FinancialContext-Pattern, optionale Structs in UseCaseIntake
2. AGG Obligations Modul: 8 Obligations (§1-§22 AGG)
   — Bias-Audit, Beweislastumkehr, Proxy-Merkmale, Beschwerdemechanismus
   — Applicability: domain=hr/recruiting, country=DE
3. Frontend: Conditional Domain-Fragen in Step 4 des UCCA-Wizard
   — HR: 6 Fragen (Screening, Absagen, AGG, Bias-Audit, Human Review)
   — Education: 5 Fragen (Noten, Pruefungen, Minderjaehrige, Lehrkraft-Review)
   — Healthcare: 6 Fragen (Diagnose, Triage, MDR, klinische Validierung)
   — Farbcodierung: rot=Risiko, gruen=Schutzmassnahme
   — Domain-Contexts im Submit-Payload gemappt

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 22:06:15 +02:00
Benjamin Admin
f17608a956 feat: EU AI Database Registration (Art. 49) — Backend + Frontend
Backend (Go):
- DB Migration 023: ai_system_registrations Tabelle
- RegistrationStore: CRUD + Status-Management + Export-JSON
- RegistrationHandlers: 7 Endpoints (Create, List, Get, Update, Status, Prefill, Export)
- Routes in main.go: /sdk/v1/ai-registration/*

Frontend (Next.js):
- 6-Step Wizard: Anbieter → System → Klassifikation → Konformitaet → Trainingsdaten → Pruefung
- System-Karten mit Status-Badges (Entwurf/Bereit/Eingereicht/Registriert)
- JSON-Export fuer EU-Datenbank-Submission
- Status-Workflow: draft → ready → submitted → registered
- API Proxy Routes

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 17:13:39 +02:00
Benjamin Admin
ce3df9f080 feat: AI Act Obligations erweitert (60→81) + Decision Tree Q8 fix
1. 21 neue AI Act Obligations:
   - Art. 9 Risk Management (5 granulare Regeln)
   - Art. 10 Data Governance (3: Bias, Qualitaet, Versionierung)
   - Art. 12 Logging (3: I/O-Logging, Manipulationsschutz, Aufbewahrung)
   - Art. 14 Human Oversight (3: Override, Schulung, Automation Bias)
   - Art. 15 Accuracy/Cybersecurity (3: Genauigkeit, Robustheit, Security)
   - Art. 51/52/54/56 GPAI Governance (4: Klassifizierung, Kennzeichnung, EU-Rep, CoP)
2. Decision Tree Q8 praezisiert:
   "Stellst du ein KI-Modell fuer Dritte bereit?" statt generische GPAI-Frage

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 16:41:29 +02:00
Benjamin Admin
2da39e035d docs: SDK-Flow + Wiki — BetrVG-Modul dokumentiert
1. SDK-Flow: Use-Case-Assessment Beschreibung aktualisiert
   - BetrVG-Toggles in Step 4 dokumentiert
   - Konflikt-Score und BAG-Urteile erwaehnt
2. Wiki: BetrVG-Artikel als SQL-Migration
   - Leitentscheidungen (M365, SAP, SaaS, Belastungsstatistik)
   - Konflikt-Score Erklaerung
   - Wird nach Compliance-Refactoring auf Production eingespielt

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 12:04:54 +02:00
Benjamin Admin
1989c410a9 test: BetrVG-Modul Tests — Konflikt-Score, Escalation, Obligations, Applicability
10 Tests: Score-Berechnung (no data, monitoring, HR, consulted),
Escalation (E2/E3 Trigger), V2-Obligations-Loading, Applicability (DE/US/small).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 11:11:33 +02:00
Benjamin Admin
c55a6ab995 feat: BetrVG-Compliance-Modul — Obligations, Konflikt-Score, Frontend
1. BetrVG Obligations (JSON V2): 12 Pflichten basierend auf §87, §90, §94, §95, §99, §111
   - BAG-Rechtsprechung referenziert (M365, SAP, Standardsoftware)
   - Applicability: DE + >=5 Mitarbeiter
2. Betriebsrats-Konflikt-Score (0-100): Gewichtete Formel aus 8 Faktoren
   - Ueberwachungseignung, HR-Bezug, Individualisierbarkeit, Automation
   - Escalation-Trigger: Score>=50 ohne BR → E2, Score>=75 → E3
3. Frontend: 3 neue Intake-Felder (Monitoring, HR, BR-Konsultation)
   - BR-Konflikt-Badge in Use-Case-Liste + Detail-Seite
   - Farbcodierung: gruen/gelb/orange/rot

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 10:49:56 +02:00
Benjamin Admin
bc75b4455d feat: AI Act Decision Tree — Zwei-Achsen-Klassifikation (GPAI + High-Risk)
Interaktiver 12-Fragen-Entscheidungsbaum für die AI Act Klassifikation
auf zwei Achsen: High-Risk (Anhang III, Q1-Q7) und GPAI (Art. 51-56, Q8-Q12).
Deterministische Auswertung ohne LLM.

Backend (Go):
- Neue Structs: GPAIClassification, DecisionTreeAnswer, DecisionTreeResult
- Decision Tree Engine mit BuildDecisionTreeDefinition() und EvaluateDecisionTree()
- Store-Methoden für CRUD der Ergebnisse
- API-Endpoints: GET/POST /decision-tree, GET/DELETE /decision-tree/results
- 12 Unit Tests (alle bestanden)

Frontend (Next.js):
- DecisionTreeWizard: Wizard-UI mit Ja/Nein-Fragen, Dual-Progress-Bar, Ergebnis-Ansicht
- AI Act Page refactored: Tabs (Übersicht | Entscheidungsbaum | Ergebnisse)
- Proxy-Route für decision-tree Endpoints

Migration 083: ai_act_decision_tree_results Tabelle

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 10:14:09 +02:00
Benjamin Admin
712fa8cb74 feat: Pass 0b quality — negative actions, container detection, session object classes
All checks were successful
CI/CD / go-lint (push) Has been skipped
CI/CD / python-lint (push) Has been skipped
CI/CD / nodejs-lint (push) Has been skipped
CI/CD / test-go-ai-compliance (push) Successful in 33s
CI/CD / test-python-backend-compliance (push) Successful in 30s
CI/CD / test-python-document-crawler (push) Successful in 21s
CI/CD / test-python-dsms-gateway (push) Successful in 16s
CI/CD / validate-canonical-controls (push) Successful in 10s
CI/CD / Deploy (push) Successful in 2s
4 error class fixes from AUTH-1052 quality review:
1. Prohibitive action types (prevent/exclude/forbid) for "dürfen keine", "verboten" etc.
2. Container object detection (Sitzungsverwaltung, Token-Schutz → _requires_decomposition)
3. Session-specific object classes (session, cookie, jwt, federated_assertion)
4. Session lifecycle actions (invalidate, issue, rotate, enforce) with templates + severity caps

76 new tests (303 total), all passing.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-28 17:24:19 +01:00
Benjamin Admin
447ec08509 Add migration 082: widen source_article to TEXT, fix pass0b query filters
All checks were successful
CI/CD / go-lint (push) Has been skipped
CI/CD / python-lint (push) Has been skipped
CI/CD / nodejs-lint (push) Has been skipped
CI/CD / test-go-ai-compliance (push) Successful in 40s
CI/CD / test-python-backend-compliance (push) Successful in 31s
CI/CD / test-python-document-crawler (push) Successful in 21s
CI/CD / test-python-dsms-gateway (push) Successful in 18s
CI/CD / validate-canonical-controls (push) Successful in 10s
CI/CD / Deploy (push) Successful in 5s
- source_article/source_regulation VARCHAR(100) → TEXT for long NIST refs
- Pass 0b NOT EXISTS queries now skip deprecated/duplicate controls
- Duplicate Guard excludes deprecated/duplicate from existence check

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-28 12:47:26 +01:00
Benjamin Admin
8cb1dc1108 Fix pass0b queries to skip deprecated/duplicate controls
The NOT EXISTS check and Duplicate Guard now exclude deprecated and
duplicate controls, enabling clean re-runs after invalidation.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-28 09:09:16 +01:00
Benjamin Admin
f8d9919b97 Improve object normalization: shorter keys, synonym expansion, qualifier stripping
- Truncate object keys to 40 chars (was 80) at underscore boundary
- Strip German qualifying prepositional phrases (bei/für/gemäß/von/zur/...)
- Add 65 new synonym mappings for near-duplicate patterns found in analysis
- Strip trailing noise tokens (articles/prepositions)
- Add _truncate_at_boundary() helper and _QUALIFYING_PHRASE_RE regex
- 11 new tests for normalization improvements (227 total pass)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-28 08:55:48 +01:00
62 changed files with 17517 additions and 466 deletions

View File

@@ -0,0 +1,47 @@
import { NextRequest, NextResponse } from 'next/server'
const SDK_URL = process.env.SDK_URL || 'http://ai-compliance-sdk:8090'
export async function GET(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
try {
const { id } = await params
const resp = await fetch(`${SDK_URL}/sdk/v1/ai-registration/${id}`)
const data = await resp.json()
return NextResponse.json(data)
} catch (err) {
return NextResponse.json({ error: 'Failed to fetch registration' }, { status: 500 })
}
}
export async function PUT(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
try {
const { id } = await params
const tenantId = request.headers.get('x-tenant-id') || '9282a473-5c95-4b3a-bf78-0ecc0ec71d3e'
const body = await request.json()
const resp = await fetch(`${SDK_URL}/sdk/v1/ai-registration/${id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json', 'X-Tenant-ID': tenantId },
body: JSON.stringify(body),
})
const data = await resp.json()
return NextResponse.json(data, { status: resp.status })
} catch (err) {
return NextResponse.json({ error: 'Failed to update registration' }, { status: 500 })
}
}
export async function PATCH(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
try {
const { id } = await params
const body = await request.json()
const resp = await fetch(`${SDK_URL}/sdk/v1/ai-registration/${id}/status`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
})
const data = await resp.json()
return NextResponse.json(data, { status: resp.status })
} catch (err) {
return NextResponse.json({ error: 'Failed to update status' }, { status: 500 })
}
}

View File

@@ -0,0 +1,32 @@
import { NextRequest, NextResponse } from 'next/server'
const SDK_URL = process.env.SDK_URL || 'http://ai-compliance-sdk:8090'
export async function GET(request: NextRequest) {
try {
const tenantId = request.headers.get('x-tenant-id') || '9282a473-5c95-4b3a-bf78-0ecc0ec71d3e'
const resp = await fetch(`${SDK_URL}/sdk/v1/ai-registration`, {
headers: { 'X-Tenant-ID': tenantId },
})
const data = await resp.json()
return NextResponse.json(data)
} catch (err) {
return NextResponse.json({ error: 'Failed to fetch registrations' }, { status: 500 })
}
}
export async function POST(request: NextRequest) {
try {
const tenantId = request.headers.get('x-tenant-id') || '9282a473-5c95-4b3a-bf78-0ecc0ec71d3e'
const body = await request.json()
const resp = await fetch(`${SDK_URL}/sdk/v1/ai-registration`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-Tenant-ID': tenantId },
body: JSON.stringify(body),
})
const data = await resp.json()
return NextResponse.json(data, { status: resp.status })
} catch (err) {
return NextResponse.json({ error: 'Failed to create registration' }, { status: 500 })
}
}

View File

@@ -0,0 +1,48 @@
import { NextRequest, NextResponse } from 'next/server'
const SDK_URL = process.env.SDK_URL || 'http://ai-compliance-sdk:8090'
export async function GET(request: NextRequest) {
try {
const { searchParams } = new URL(request.url)
const endpoint = searchParams.get('endpoint') || 'controls'
const tenantId = request.headers.get('x-tenant-id') || '9282a473-5c95-4b3a-bf78-0ecc0ec71d3e'
let path: string
switch (endpoint) {
case 'controls':
const domain = searchParams.get('domain') || ''
path = `/sdk/v1/payment-compliance/controls${domain ? `?domain=${domain}` : ''}`
break
case 'assessments':
path = '/sdk/v1/payment-compliance/assessments'
break
default:
path = '/sdk/v1/payment-compliance/controls'
}
const resp = await fetch(`${SDK_URL}${path}`, {
headers: { 'X-Tenant-ID': tenantId },
})
const data = await resp.json()
return NextResponse.json(data)
} catch (err) {
return NextResponse.json({ error: 'Failed to fetch' }, { status: 500 })
}
}
export async function POST(request: NextRequest) {
try {
const tenantId = request.headers.get('x-tenant-id') || '9282a473-5c95-4b3a-bf78-0ecc0ec71d3e'
const body = await request.json()
const resp = await fetch(`${SDK_URL}/sdk/v1/payment-compliance/assessments`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-Tenant-ID': tenantId },
body: JSON.stringify(body),
})
const data = await resp.json()
return NextResponse.json(data, { status: resp.status })
} catch (err) {
return NextResponse.json({ error: 'Failed to create' }, { status: 500 })
}
}

View File

@@ -0,0 +1,28 @@
import { NextRequest, NextResponse } from 'next/server'
const SDK_URL = process.env.SDK_URL || 'http://ai-compliance-sdk:8090'
export async function GET(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
try {
const { id } = await params
const resp = await fetch(`${SDK_URL}/sdk/v1/payment-compliance/tender/${id}`)
return NextResponse.json(await resp.json())
} catch {
return NextResponse.json({ error: 'Failed' }, { status: 500 })
}
}
export async function POST(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
try {
const { id } = await params
const { searchParams } = new URL(request.url)
const action = searchParams.get('action') || 'extract'
const resp = await fetch(`${SDK_URL}/sdk/v1/payment-compliance/tender/${id}/${action}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
})
return NextResponse.json(await resp.json(), { status: resp.status })
} catch {
return NextResponse.json({ error: 'Failed' }, { status: 500 })
}
}

View File

@@ -0,0 +1,30 @@
import { NextRequest, NextResponse } from 'next/server'
const SDK_URL = process.env.SDK_URL || 'http://ai-compliance-sdk:8090'
export async function GET(request: NextRequest) {
try {
const tenantId = request.headers.get('x-tenant-id') || '9282a473-5c95-4b3a-bf78-0ecc0ec71d3e'
const resp = await fetch(`${SDK_URL}/sdk/v1/payment-compliance/tender`, {
headers: { 'X-Tenant-ID': tenantId },
})
return NextResponse.json(await resp.json())
} catch {
return NextResponse.json({ error: 'Failed' }, { status: 500 })
}
}
export async function POST(request: NextRequest) {
try {
const tenantId = request.headers.get('x-tenant-id') || '9282a473-5c95-4b3a-bf78-0ecc0ec71d3e'
const formData = await request.formData()
const resp = await fetch(`${SDK_URL}/sdk/v1/payment-compliance/tender/upload`, {
method: 'POST',
headers: { 'X-Tenant-ID': tenantId },
body: formData,
})
return NextResponse.json(await resp.json(), { status: resp.status })
} catch {
return NextResponse.json({ error: 'Upload failed' }, { status: 500 })
}
}

View File

@@ -0,0 +1,57 @@
import { NextRequest, NextResponse } from 'next/server'
const SDK_URL = process.env.SDK_URL || 'http://ai-compliance-sdk:8090'
const DEFAULT_TENANT = process.env.DEFAULT_TENANT_ID || '9282a473-5c95-4b3a-bf78-0ecc0ec71d3e'
/**
* Proxy: /api/sdk/v1/ucca/decision-tree/... → Go Backend /sdk/v1/ucca/decision-tree/...
*/
async function proxyRequest(request: NextRequest, { params }: { params: Promise<{ path: string[] }> }) {
const { path } = await params
const subPath = path ? path.join('/') : ''
const search = request.nextUrl.search || ''
const targetUrl = `${SDK_URL}/sdk/v1/ucca/decision-tree/${subPath}${search}`
const tenantID = request.headers.get('X-Tenant-ID') || DEFAULT_TENANT
try {
const headers: Record<string, string> = {
'X-Tenant-ID': tenantID,
}
const fetchOptions: RequestInit = {
method: request.method,
headers,
}
if (request.method === 'POST' || request.method === 'PUT' || request.method === 'PATCH') {
const body = await request.json()
headers['Content-Type'] = 'application/json'
fetchOptions.body = JSON.stringify(body)
}
const response = await fetch(targetUrl, fetchOptions)
if (!response.ok) {
const errorText = await response.text()
console.error(`Decision tree proxy error [${request.method} ${subPath}]:`, errorText)
return NextResponse.json(
{ error: 'Backend error', details: errorText },
{ status: response.status }
)
}
const data = await response.json()
return NextResponse.json(data, { status: response.status })
} catch (error) {
console.error('Decision tree proxy connection error:', error)
return NextResponse.json(
{ error: 'Failed to connect to AI compliance backend' },
{ status: 503 }
)
}
}
export const GET = proxyRequest
export const POST = proxyRequest
export const DELETE = proxyRequest

View File

@@ -0,0 +1,36 @@
import { NextRequest, NextResponse } from 'next/server'
const SDK_URL = process.env.SDK_URL || 'http://ai-compliance-sdk:8090'
const DEFAULT_TENANT = process.env.DEFAULT_TENANT_ID || '9282a473-5c95-4b3a-bf78-0ecc0ec71d3e'
/**
* Proxy: GET /api/sdk/v1/ucca/decision-tree → Go Backend GET /sdk/v1/ucca/decision-tree
* Returns the decision tree definition (questions, structure)
*/
export async function GET(request: NextRequest) {
const tenantID = request.headers.get('X-Tenant-ID') || DEFAULT_TENANT
try {
const response = await fetch(`${SDK_URL}/sdk/v1/ucca/decision-tree`, {
headers: { 'X-Tenant-ID': tenantID },
})
if (!response.ok) {
const errorText = await response.text()
console.error('Decision tree GET error:', errorText)
return NextResponse.json(
{ error: 'Backend error', details: errorText },
{ status: response.status }
)
}
const data = await response.json()
return NextResponse.json(data)
} catch (error) {
console.error('Decision tree proxy error:', error)
return NextResponse.json(
{ error: 'Failed to connect to AI compliance backend' },
{ status: 503 }
)
}
}

View File

@@ -333,6 +333,71 @@ function AdvisoryBoardPageInner() {
purposes: [] as string[],
// Automation (single-select tile)
automation: '' as string,
// BetrVG / works council
employee_monitoring: false,
hr_decision_support: false,
works_council_consulted: false,
// Domain-specific contexts (Annex III)
hr_automated_screening: false,
hr_automated_rejection: false,
hr_candidate_ranking: false,
hr_bias_audits: false,
hr_agg_visible: false,
hr_human_review: false,
hr_performance_eval: false,
edu_grade_influence: false,
edu_exam_evaluation: false,
edu_student_selection: false,
edu_minors: false,
edu_teacher_review: false,
hc_diagnosis: false,
hc_treatment: false,
hc_triage: false,
hc_patient_data: false,
hc_medical_device: false,
hc_clinical_validation: false,
// Legal
leg_legal_advice: false, leg_court_prediction: false, leg_client_confidential: false,
// Public Sector
pub_admin_decision: false, pub_benefit_allocation: false, pub_transparency: false,
// Critical Infrastructure
crit_grid_control: false, crit_safety_critical: false, crit_redundancy: false,
// Automotive
auto_autonomous: false, auto_safety: false, auto_functional_safety: false,
// Retail
ret_pricing: false, ret_profiling: false, ret_credit_scoring: false, ret_dark_patterns: false,
// IT Security
its_surveillance: false, its_threat_detection: false, its_data_retention: false,
// Logistics
log_driver_tracking: false, log_workload_scoring: false,
// Construction
con_tenant_screening: false, con_worker_safety: false,
// Marketing
mkt_deepfake: false, mkt_minors: false, mkt_targeting: false, mkt_labeled: false,
// Manufacturing
mfg_machine_safety: false, mfg_ce_required: false, mfg_validated: false,
// Agriculture
agr_pesticide: false, agr_animal_welfare: false, agr_environmental: false,
// Social Services
soc_vulnerable: false, soc_benefit: false, soc_case_mgmt: false,
// Hospitality
hos_guest_profiling: false, hos_dynamic_pricing: false, hos_review_manipulation: false,
// Insurance
ins_risk_class: false, ins_claims: false, ins_premium: false, ins_fraud: false,
// Investment
inv_algo_trading: false, inv_advice: false, inv_robo: false,
// Defense
def_dual_use: false, def_export: false, def_classified: false,
// Supply Chain
sch_supplier: false, sch_human_rights: false, sch_environmental: false,
// Facility
fac_access: false, fac_occupancy: false, fac_energy: false,
// Sports
spo_athlete: false, spo_fan: false, spo_doping: false,
// Finance / Banking
fin_credit_scoring: false, fin_aml_kyc: false, fin_algo_decisions: false, fin_customer_profiling: false,
// General
gen_affects_people: false, gen_automated_decisions: false, gen_sensitive_data: false,
// Hosting (single-select tile)
hosting_provider: '' as string,
hosting_region: '' as string,
@@ -420,7 +485,131 @@ function AdvisoryBoardPageInner() {
retention_purpose: form.retention_purpose,
contracts_list: form.contracts,
subprocessors: form.subprocessors,
employee_monitoring: form.employee_monitoring,
hr_decision_support: form.hr_decision_support,
works_council_consulted: form.works_council_consulted,
// Domain-specific contexts
hr_context: ['hr', 'recruiting'].includes(form.domain) ? {
automated_screening: form.hr_automated_screening,
automated_rejection: form.hr_automated_rejection,
candidate_ranking: form.hr_candidate_ranking,
bias_audits_done: form.hr_bias_audits,
agg_categories_visible: form.hr_agg_visible,
human_review_enforced: form.hr_human_review,
performance_evaluation: form.hr_performance_eval,
} : undefined,
education_context: ['education', 'higher_education', 'vocational_training', 'research'].includes(form.domain) ? {
grade_influence: form.edu_grade_influence,
exam_evaluation: form.edu_exam_evaluation,
student_selection: form.edu_student_selection,
minors_involved: form.edu_minors,
teacher_review_required: form.edu_teacher_review,
} : undefined,
healthcare_context: ['healthcare', 'medical_devices', 'pharma', 'elderly_care'].includes(form.domain) ? {
diagnosis_support: form.hc_diagnosis,
treatment_recommendation: form.hc_treatment,
triage_decision: form.hc_triage,
patient_data_processed: form.hc_patient_data,
medical_device: form.hc_medical_device,
clinical_validation: form.hc_clinical_validation,
} : undefined,
legal_context: ['legal', 'consulting', 'tax_advisory'].includes(form.domain) ? {
legal_advice: form.leg_legal_advice,
court_prediction: form.leg_court_prediction,
client_confidential: form.leg_client_confidential,
} : undefined,
public_sector_context: ['public_sector', 'defense', 'justice'].includes(form.domain) ? {
admin_decision: form.pub_admin_decision,
benefit_allocation: form.pub_benefit_allocation,
transparency_ensured: form.pub_transparency,
} : undefined,
critical_infra_context: ['energy', 'utilities', 'oil_gas'].includes(form.domain) ? {
grid_control: form.crit_grid_control,
safety_critical: form.crit_safety_critical,
redundancy_exists: form.crit_redundancy,
} : undefined,
automotive_context: ['automotive', 'aerospace'].includes(form.domain) ? {
autonomous_driving: form.auto_autonomous,
safety_relevant: form.auto_safety,
functional_safety: form.auto_functional_safety,
} : undefined,
retail_context: ['retail', 'ecommerce', 'wholesale'].includes(form.domain) ? {
pricing_personalized: form.ret_pricing,
credit_scoring: form.ret_credit_scoring,
dark_patterns: form.ret_dark_patterns,
} : undefined,
it_security_context: ['it_services', 'cybersecurity', 'telecom'].includes(form.domain) ? {
employee_surveillance: form.its_surveillance,
threat_detection: form.its_threat_detection,
data_retention_logs: form.its_data_retention,
} : undefined,
logistics_context: ['logistics'].includes(form.domain) ? {
driver_tracking: form.log_driver_tracking,
workload_scoring: form.log_workload_scoring,
} : undefined,
construction_context: ['construction', 'real_estate', 'facility_management'].includes(form.domain) ? {
tenant_screening: form.con_tenant_screening,
worker_safety: form.con_worker_safety,
} : undefined,
marketing_context: ['marketing', 'media', 'entertainment'].includes(form.domain) ? {
deepfake_content: form.mkt_deepfake,
behavioral_targeting: form.mkt_targeting,
minors_targeted: form.mkt_minors,
ai_content_labeled: form.mkt_labeled,
} : undefined,
manufacturing_context: ['mechanical_engineering', 'electrical_engineering', 'plant_engineering', 'chemicals', 'food_beverage'].includes(form.domain) ? {
machine_safety: form.mfg_machine_safety,
ce_marking_required: form.mfg_ce_required,
safety_validated: form.mfg_validated,
} : undefined,
agriculture_context: ['agriculture', 'forestry', 'fishing'].includes(form.domain) ? {
pesticide_ai: form.agr_pesticide,
animal_welfare: form.agr_animal_welfare,
environmental_data: form.agr_environmental,
} : undefined,
social_services_context: ['social_services', 'nonprofit'].includes(form.domain) ? {
vulnerable_groups: form.soc_vulnerable,
benefit_decision: form.soc_benefit,
case_management: form.soc_case_mgmt,
} : undefined,
hospitality_context: ['hospitality', 'tourism'].includes(form.domain) ? {
guest_profiling: form.hos_guest_profiling,
dynamic_pricing: form.hos_dynamic_pricing,
review_manipulation: form.hos_review_manipulation,
} : undefined,
insurance_context: ['insurance'].includes(form.domain) ? {
risk_classification: form.ins_risk_class,
claims_automation: form.ins_claims,
premium_calculation: form.ins_premium,
fraud_detection: form.ins_fraud,
} : undefined,
investment_context: ['investment'].includes(form.domain) ? {
algo_trading: form.inv_algo_trading,
investment_advice: form.inv_advice,
robo_advisor: form.inv_robo,
} : undefined,
defense_context: ['defense'].includes(form.domain) ? {
dual_use: form.def_dual_use,
export_controlled: form.def_export,
classified_data: form.def_classified,
} : undefined,
supply_chain_context: ['textiles', 'packaging'].includes(form.domain) ? {
supplier_monitoring: form.sch_supplier,
human_rights_check: form.sch_human_rights,
environmental_impact: form.sch_environmental,
} : undefined,
facility_context: ['facility_management'].includes(form.domain) ? {
access_control_ai: form.fac_access,
occupancy_tracking: form.fac_occupancy,
energy_optimization: form.fac_energy,
} : undefined,
sports_context: ['sports'].includes(form.domain) ? {
athlete_tracking: form.spo_athlete,
fan_profiling: form.spo_fan,
} : undefined,
store_raw_text: true,
// Finance/Banking and General don't need separate context structs —
// their fields are evaluated via existing FinancialContext or generic rules
}
const url = isEditMode
@@ -777,6 +966,567 @@ function AdvisoryBoardPageInner() {
Informationspflicht, Recht auf menschliche Ueberpruefung und Anfechtungsmoeglichkeit.
</p>
</div>
{/* BetrVG Section */}
<div className="mt-6 pt-6 border-t border-gray-200">
<h3 className="text-sm font-semibold text-gray-900 mb-1">Betriebsrat & Beschaeftigtendaten</h3>
<p className="text-xs text-gray-500 mb-4">
Relevant fuer deutsche Unternehmen mit Betriebsrat (§87 Abs.1 Nr.6 BetrVG).
</p>
<div className="space-y-3">
<label className="flex items-center gap-3 p-3 rounded-lg border border-gray-200 hover:bg-gray-50 cursor-pointer">
<input
type="checkbox"
checked={form.employee_monitoring}
onChange={(e) => updateForm({ employee_monitoring: e.target.checked })}
className="w-4 h-4 rounded border-gray-300 text-purple-600 focus:ring-purple-500"
/>
<div>
<span className="text-sm font-medium text-gray-900">System kann Verhalten/Leistung ueberwachen</span>
<p className="text-xs text-gray-500">Nutzungslogs, Produktivitaetskennzahlen, Kommunikationsanalyse</p>
</div>
</label>
<label className="flex items-center gap-3 p-3 rounded-lg border border-gray-200 hover:bg-gray-50 cursor-pointer">
<input
type="checkbox"
checked={form.hr_decision_support}
onChange={(e) => updateForm({ hr_decision_support: e.target.checked })}
className="w-4 h-4 rounded border-gray-300 text-purple-600 focus:ring-purple-500"
/>
<div>
<span className="text-sm font-medium text-gray-900">System unterstuetzt HR-Entscheidungen</span>
<p className="text-xs text-gray-500">Recruiting, Bewertung, Befoerderung, Kuendigung</p>
</div>
</label>
<label className="flex items-center gap-3 p-3 rounded-lg border border-gray-200 hover:bg-gray-50 cursor-pointer">
<input
type="checkbox"
checked={form.works_council_consulted}
onChange={(e) => updateForm({ works_council_consulted: e.target.checked })}
className="w-4 h-4 rounded border-gray-300 text-purple-600 focus:ring-purple-500"
/>
<div>
<span className="text-sm font-medium text-gray-900">Betriebsrat wurde konsultiert</span>
<p className="text-xs text-gray-500">Betriebsvereinbarung liegt vor oder ist in Verhandlung</p>
</div>
</label>
</div>
</div>
{/* Domain-specific questions — HR/Recruiting */}
{['hr', 'recruiting'].includes(form.domain) && (
<div className="mt-6 pt-6 border-t border-gray-200">
<h3 className="text-sm font-semibold text-gray-900 mb-1">HR & Recruiting Hochrisiko-Pruefung</h3>
<p className="text-xs text-gray-500 mb-4">AI Act Annex III Nr. 4 + AGG Pflichtfragen bei KI im Personalbereich.</p>
<div className="space-y-3">
<label className="flex items-center gap-3 p-3 rounded-lg border border-gray-200 hover:bg-gray-50 cursor-pointer">
<input type="checkbox" checked={form.hr_automated_screening} onChange={(e) => updateForm({ hr_automated_screening: e.target.checked })} className="w-4 h-4 rounded border-gray-300 text-purple-600 focus:ring-purple-500" />
<div><span className="text-sm font-medium text-gray-900">Bewerber werden automatisch vorsortiert/gerankt</span><p className="text-xs text-gray-500">CV-Screening, Score-basierte Vorauswahl</p></div>
</label>
<label className="flex items-center gap-3 p-3 rounded-lg border border-red-200 bg-red-50 hover:bg-red-100 cursor-pointer">
<input type="checkbox" checked={form.hr_automated_rejection} onChange={(e) => updateForm({ hr_automated_rejection: e.target.checked })} className="w-4 h-4 rounded border-red-300 text-red-600 focus:ring-red-500" />
<div><span className="text-sm font-medium text-red-900">Absagen werden automatisch versendet</span><p className="text-xs text-red-700">Art. 22 DSGVO: Vollautomatische Absagen grundsaetzlich unzulaessig!</p></div>
</label>
<label className="flex items-center gap-3 p-3 rounded-lg border border-gray-200 hover:bg-gray-50 cursor-pointer">
<input type="checkbox" checked={form.hr_agg_visible} onChange={(e) => updateForm({ hr_agg_visible: e.target.checked })} className="w-4 h-4 rounded border-gray-300 text-purple-600 focus:ring-purple-500" />
<div><span className="text-sm font-medium text-gray-900">System kann AGG-Merkmale erkennen (Name, Foto, Alter)</span><p className="text-xs text-gray-500">Proxy-Diskriminierung: NameHerkunft, FotoGeschlecht</p></div>
</label>
<label className="flex items-center gap-3 p-3 rounded-lg border border-gray-200 hover:bg-gray-50 cursor-pointer">
<input type="checkbox" checked={form.hr_performance_eval} onChange={(e) => updateForm({ hr_performance_eval: e.target.checked })} className="w-4 h-4 rounded border-gray-300 text-purple-600 focus:ring-purple-500" />
<div><span className="text-sm font-medium text-gray-900">System bewertet Mitarbeiterleistung</span><p className="text-xs text-gray-500">Performance Reviews, KPI-Tracking</p></div>
</label>
<label className="flex items-center gap-3 p-3 rounded-lg border border-green-200 bg-green-50 hover:bg-green-100 cursor-pointer">
<input type="checkbox" checked={form.hr_bias_audits} onChange={(e) => updateForm({ hr_bias_audits: e.target.checked })} className="w-4 h-4 rounded border-green-300 text-green-600 focus:ring-green-500" />
<div><span className="text-sm font-medium text-green-900">Regelmaessige Bias-Audits durchgefuehrt</span><p className="text-xs text-green-700">Analyse nach Geschlecht, Alter, Herkunft</p></div>
</label>
<label className="flex items-center gap-3 p-3 rounded-lg border border-green-200 bg-green-50 hover:bg-green-100 cursor-pointer">
<input type="checkbox" checked={form.hr_human_review} onChange={(e) => updateForm({ hr_human_review: e.target.checked })} className="w-4 h-4 rounded border-green-300 text-green-600 focus:ring-green-500" />
<div><span className="text-sm font-medium text-green-900">Mensch prueft jede KI-Empfehlung</span><p className="text-xs text-green-700">Kein Rubber Stamping echte Pruefung</p></div>
</label>
</div>
</div>
)}
{/* Domain-specific questions — Education */}
{['education', 'higher_education', 'vocational_training', 'research'].includes(form.domain) && (
<div className="mt-6 pt-6 border-t border-gray-200">
<h3 className="text-sm font-semibold text-gray-900 mb-1">Bildung Hochrisiko-Pruefung</h3>
<p className="text-xs text-gray-500 mb-4">AI Act Annex III Nr. 3 bei KI in Bildung und Ausbildung.</p>
<div className="space-y-3">
<label className="flex items-center gap-3 p-3 rounded-lg border border-gray-200 hover:bg-gray-50 cursor-pointer">
<input type="checkbox" checked={form.edu_grade_influence} onChange={(e) => updateForm({ edu_grade_influence: e.target.checked })} className="w-4 h-4 rounded border-gray-300 text-purple-600 focus:ring-purple-500" />
<div><span className="text-sm font-medium text-gray-900">KI beeinflusst Noten oder Bewertungen</span><p className="text-xs text-gray-500">Notenvorschlaege, Bewertungsunterstuetzung</p></div>
</label>
<label className="flex items-center gap-3 p-3 rounded-lg border border-gray-200 hover:bg-gray-50 cursor-pointer">
<input type="checkbox" checked={form.edu_exam_evaluation} onChange={(e) => updateForm({ edu_exam_evaluation: e.target.checked })} className="w-4 h-4 rounded border-gray-300 text-purple-600 focus:ring-purple-500" />
<div><span className="text-sm font-medium text-gray-900">KI bewertet Pruefungen/Klausuren</span><p className="text-xs text-gray-500">Automatische Korrektur, Bewertungsvorschlaege</p></div>
</label>
<label className="flex items-center gap-3 p-3 rounded-lg border border-gray-200 hover:bg-gray-50 cursor-pointer">
<input type="checkbox" checked={form.edu_student_selection} onChange={(e) => updateForm({ edu_student_selection: e.target.checked })} className="w-4 h-4 rounded border-gray-300 text-purple-600 focus:ring-purple-500" />
<div><span className="text-sm font-medium text-gray-900">KI beeinflusst Zugang zu Bildungsangeboten</span><p className="text-xs text-gray-500">Zulassung, Kursempfehlungen, Einstufung</p></div>
</label>
<label className="flex items-center gap-3 p-3 rounded-lg border border-red-200 bg-red-50 hover:bg-red-100 cursor-pointer">
<input type="checkbox" checked={form.edu_minors} onChange={(e) => updateForm({ edu_minors: e.target.checked })} className="w-4 h-4 rounded border-red-300 text-red-600 focus:ring-red-500" />
<div><span className="text-sm font-medium text-red-900">Minderjaehrige sind betroffen</span><p className="text-xs text-red-700">Besonderer Schutz (Art. 24 EU-Grundrechtecharta)</p></div>
</label>
<label className="flex items-center gap-3 p-3 rounded-lg border border-green-200 bg-green-50 hover:bg-green-100 cursor-pointer">
<input type="checkbox" checked={form.edu_teacher_review} onChange={(e) => updateForm({ edu_teacher_review: e.target.checked })} className="w-4 h-4 rounded border-green-300 text-green-600 focus:ring-green-500" />
<div><span className="text-sm font-medium text-green-900">Lehrkraft prueft jedes KI-Ergebnis</span><p className="text-xs text-green-700">Human Oversight vor Mitteilung an Schueler</p></div>
</label>
</div>
</div>
)}
{/* Domain-specific questions — Healthcare */}
{['healthcare', 'medical_devices', 'pharma', 'elderly_care'].includes(form.domain) && (
<div className="mt-6 pt-6 border-t border-gray-200">
<h3 className="text-sm font-semibold text-gray-900 mb-1">Gesundheitswesen Hochrisiko-Pruefung</h3>
<p className="text-xs text-gray-500 mb-4">AI Act Annex III Nr. 5 + MDR (EU) 2017/745.</p>
<div className="space-y-3">
<label className="flex items-center gap-3 p-3 rounded-lg border border-gray-200 hover:bg-gray-50 cursor-pointer">
<input type="checkbox" checked={form.hc_diagnosis} onChange={(e) => updateForm({ hc_diagnosis: e.target.checked })} className="w-4 h-4 rounded border-gray-300 text-purple-600 focus:ring-purple-500" />
<div><span className="text-sm font-medium text-gray-900">KI unterstuetzt Diagnosen</span><p className="text-xs text-gray-500">Diagnosevorschlaege, Bildgebungsauswertung</p></div>
</label>
<label className="flex items-center gap-3 p-3 rounded-lg border border-gray-200 hover:bg-gray-50 cursor-pointer">
<input type="checkbox" checked={form.hc_treatment} onChange={(e) => updateForm({ hc_treatment: e.target.checked })} className="w-4 h-4 rounded border-gray-300 text-purple-600 focus:ring-purple-500" />
<div><span className="text-sm font-medium text-gray-900">KI empfiehlt Behandlungen</span><p className="text-xs text-gray-500">Therapievorschlaege, Medikation</p></div>
</label>
<label className="flex items-center gap-3 p-3 rounded-lg border border-red-200 bg-red-50 hover:bg-red-100 cursor-pointer">
<input type="checkbox" checked={form.hc_triage} onChange={(e) => updateForm({ hc_triage: e.target.checked })} className="w-4 h-4 rounded border-red-300 text-red-600 focus:ring-red-500" />
<div><span className="text-sm font-medium text-red-900">KI priorisiert Patienten (Triage)</span><p className="text-xs text-red-700">Lebenskritisch erhoehte Anforderungen</p></div>
</label>
<label className="flex items-center gap-3 p-3 rounded-lg border border-gray-200 hover:bg-gray-50 cursor-pointer">
<input type="checkbox" checked={form.hc_patient_data} onChange={(e) => updateForm({ hc_patient_data: e.target.checked })} className="w-4 h-4 rounded border-gray-300 text-purple-600 focus:ring-purple-500" />
<div><span className="text-sm font-medium text-gray-900">Gesundheitsdaten verarbeitet</span><p className="text-xs text-gray-500">Art. 9 DSGVO besondere Kategorie</p></div>
</label>
<label className="flex items-center gap-3 p-3 rounded-lg border border-gray-200 hover:bg-gray-50 cursor-pointer">
<input type="checkbox" checked={form.hc_medical_device} onChange={(e) => updateForm({ hc_medical_device: e.target.checked })} className="w-4 h-4 rounded border-gray-300 text-purple-600 focus:ring-purple-500" />
<div><span className="text-sm font-medium text-gray-900">System ist Medizinprodukt (MDR)</span><p className="text-xs text-gray-500">MDR (EU) 2017/745 Zertifizierung erforderlich</p></div>
</label>
<label className="flex items-center gap-3 p-3 rounded-lg border border-green-200 bg-green-50 hover:bg-green-100 cursor-pointer">
<input type="checkbox" checked={form.hc_clinical_validation} onChange={(e) => updateForm({ hc_clinical_validation: e.target.checked })} className="w-4 h-4 rounded border-green-300 text-green-600 focus:ring-green-500" />
<div><span className="text-sm font-medium text-green-900">Klinisch validiert</span><p className="text-xs text-green-700">System wurde in klinischer Studie geprueft</p></div>
</label>
</div>
</div>
)}
{/* Legal / Justice */}
{['legal', 'consulting', 'tax_advisory'].includes(form.domain) && (
<div className="mt-6 pt-6 border-t border-gray-200">
<h3 className="text-sm font-semibold text-gray-900 mb-1">Recht & Beratung Compliance-Fragen</h3>
<p className="text-xs text-gray-500 mb-4">AI Act Annex III Nr. 8 KI in Rechtspflege und Demokratie.</p>
<div className="space-y-3">
<label className="flex items-center gap-3 p-3 rounded-lg border border-gray-200 hover:bg-gray-50 cursor-pointer">
<input type="checkbox" checked={form.leg_legal_advice} onChange={(e) => updateForm({ leg_legal_advice: e.target.checked })} className="w-4 h-4 rounded border-gray-300 text-purple-600 focus:ring-purple-500" />
<div><span className="text-sm font-medium text-gray-900">KI gibt Rechtsberatung oder rechtliche Empfehlungen</span><p className="text-xs text-gray-500">Vertragsanalyse, rechtliche Einschaetzungen, Compliance-Checks</p></div>
</label>
<label className="flex items-center gap-3 p-3 rounded-lg border border-gray-200 hover:bg-gray-50 cursor-pointer">
<input type="checkbox" checked={form.leg_court_prediction} onChange={(e) => updateForm({ leg_court_prediction: e.target.checked })} className="w-4 h-4 rounded border-gray-300 text-purple-600 focus:ring-purple-500" />
<div><span className="text-sm font-medium text-gray-900">KI prognostiziert Verfahrensausgaenge</span><p className="text-xs text-gray-500">Urteilsprognosen, Risikoeinschaetzung von Rechtsstreitigkeiten</p></div>
</label>
<label className="flex items-center gap-3 p-3 rounded-lg border border-gray-200 hover:bg-gray-50 cursor-pointer">
<input type="checkbox" checked={form.leg_client_confidential} onChange={(e) => updateForm({ leg_client_confidential: e.target.checked })} className="w-4 h-4 rounded border-gray-300 text-purple-600 focus:ring-purple-500" />
<div><span className="text-sm font-medium text-gray-900">Mandantengeheimnis betroffen</span><p className="text-xs text-gray-500">Vertrauliche Mandantendaten werden durch KI verarbeitet (§ 203 StGB)</p></div>
</label>
</div>
</div>
)}
{/* Public Sector */}
{['public_sector', 'defense', 'justice'].includes(form.domain) && (
<div className="mt-6 pt-6 border-t border-gray-200">
<h3 className="text-sm font-semibold text-gray-900 mb-1">Oeffentlicher Sektor Compliance-Fragen</h3>
<p className="text-xs text-gray-500 mb-4">Art. 27 AI Act FRIA-Pflicht fuer oeffentliche Stellen.</p>
<div className="space-y-3">
<label className="flex items-center gap-3 p-3 rounded-lg border border-red-200 bg-red-50 hover:bg-red-100 cursor-pointer">
<input type="checkbox" checked={form.pub_admin_decision} onChange={(e) => updateForm({ pub_admin_decision: e.target.checked })} className="w-4 h-4 rounded border-red-300 text-red-600 focus:ring-red-500" />
<div><span className="text-sm font-medium text-red-900">KI beeinflusst Verwaltungsentscheidungen</span><p className="text-xs text-red-700">Bescheide, Bewilligungen, Genehmigungen FRIA erforderlich</p></div>
</label>
<label className="flex items-center gap-3 p-3 rounded-lg border border-gray-200 hover:bg-gray-50 cursor-pointer">
<input type="checkbox" checked={form.pub_benefit_allocation} onChange={(e) => updateForm({ pub_benefit_allocation: e.target.checked })} className="w-4 h-4 rounded border-gray-300 text-purple-600 focus:ring-purple-500" />
<div><span className="text-sm font-medium text-gray-900">KI verteilt Leistungen oder Foerderung</span><p className="text-xs text-gray-500">Sozialleistungen, Subventionen, Zuteilungen</p></div>
</label>
<label className="flex items-center gap-3 p-3 rounded-lg border border-green-200 bg-green-50 hover:bg-green-100 cursor-pointer">
<input type="checkbox" checked={form.pub_transparency} onChange={(e) => updateForm({ pub_transparency: e.target.checked })} className="w-4 h-4 rounded border-green-300 text-green-600 focus:ring-green-500" />
<div><span className="text-sm font-medium text-green-900">Transparenz gegenueber Buergern sichergestellt</span><p className="text-xs text-green-700">Buerger werden ueber KI-Nutzung informiert</p></div>
</label>
</div>
</div>
)}
{/* Critical Infrastructure */}
{['energy', 'utilities', 'oil_gas'].includes(form.domain) && (
<div className="mt-6 pt-6 border-t border-gray-200">
<h3 className="text-sm font-semibold text-gray-900 mb-1">Kritische Infrastruktur Compliance-Fragen</h3>
<p className="text-xs text-gray-500 mb-4">AI Act Annex III Nr. 2 + NIS2.</p>
<div className="space-y-3">
<label className="flex items-center gap-3 p-3 rounded-lg border border-gray-200 hover:bg-gray-50 cursor-pointer">
<input type="checkbox" checked={form.crit_grid_control} onChange={(e) => updateForm({ crit_grid_control: e.target.checked })} className="w-4 h-4 rounded border-gray-300 text-purple-600 focus:ring-purple-500" />
<div><span className="text-sm font-medium text-gray-900">KI steuert Netz oder Infrastruktur</span><p className="text-xs text-gray-500">Stromnetz, Wasserversorgung, Gasverteilung</p></div>
</label>
<label className="flex items-center gap-3 p-3 rounded-lg border border-red-200 bg-red-50 hover:bg-red-100 cursor-pointer">
<input type="checkbox" checked={form.crit_safety_critical} onChange={(e) => updateForm({ crit_safety_critical: e.target.checked })} className="w-4 h-4 rounded border-red-300 text-red-600 focus:ring-red-500" />
<div><span className="text-sm font-medium text-red-900">Sicherheitskritische Steuerung</span><p className="text-xs text-red-700">Fehler koennen Menschenleben gefaehrden</p></div>
</label>
<label className="flex items-center gap-3 p-3 rounded-lg border border-green-200 bg-green-50 hover:bg-green-100 cursor-pointer">
<input type="checkbox" checked={form.crit_redundancy} onChange={(e) => updateForm({ crit_redundancy: e.target.checked })} className="w-4 h-4 rounded border-green-300 text-green-600 focus:ring-green-500" />
<div><span className="text-sm font-medium text-green-900">Redundante Systeme vorhanden</span><p className="text-xs text-green-700">Fallback bei KI-Ausfall sichergestellt</p></div>
</label>
</div>
</div>
)}
{/* Automotive / Aerospace */}
{['automotive', 'aerospace'].includes(form.domain) && (
<div className="mt-6 pt-6 border-t border-gray-200">
<h3 className="text-sm font-semibold text-gray-900 mb-1">Automotive / Aerospace Compliance-Fragen</h3>
<p className="text-xs text-gray-500 mb-4">Safety-critical AI Typgenehmigung + Functional Safety.</p>
<div className="space-y-3">
<label className="flex items-center gap-3 p-3 rounded-lg border border-red-200 bg-red-50 hover:bg-red-100 cursor-pointer">
<input type="checkbox" checked={form.auto_autonomous} onChange={(e) => updateForm({ auto_autonomous: e.target.checked })} className="w-4 h-4 rounded border-red-300 text-red-600 focus:ring-red-500" />
<div><span className="text-sm font-medium text-red-900">Autonomes Fahren / ADAS</span><p className="text-xs text-red-700">Hochrisiko erfordert Typgenehmigung und extensive Validierung</p></div>
</label>
<label className="flex items-center gap-3 p-3 rounded-lg border border-gray-200 hover:bg-gray-50 cursor-pointer">
<input type="checkbox" checked={form.auto_safety} onChange={(e) => updateForm({ auto_safety: e.target.checked })} className="w-4 h-4 rounded border-gray-300 text-purple-600 focus:ring-purple-500" />
<div><span className="text-sm font-medium text-gray-900">Sicherheitsrelevante Funktion</span><p className="text-xs text-gray-500">Bremsen, Lenkung, Kollisionsvermeidung</p></div>
</label>
<label className="flex items-center gap-3 p-3 rounded-lg border border-green-200 bg-green-50 hover:bg-green-100 cursor-pointer">
<input type="checkbox" checked={form.auto_functional_safety} onChange={(e) => updateForm({ auto_functional_safety: e.target.checked })} className="w-4 h-4 rounded border-green-300 text-green-600 focus:ring-green-500" />
<div><span className="text-sm font-medium text-green-900">ISO 26262 Functional Safety beruecksichtigt</span><p className="text-xs text-green-700">ASIL-Einstufung und Sicherheitsvalidierung durchgefuehrt</p></div>
</label>
</div>
</div>
)}
{/* Retail / E-Commerce */}
{['retail', 'ecommerce', 'wholesale'].includes(form.domain) && (
<div className="mt-6 pt-6 border-t border-gray-200">
<h3 className="text-sm font-semibold text-gray-900 mb-1">Handel & E-Commerce Compliance-Fragen</h3>
<p className="text-xs text-gray-500 mb-4">DSA, Verbraucherrecht, DSGVO Art. 22.</p>
<div className="space-y-3">
<label className="flex items-center gap-3 p-3 rounded-lg border border-gray-200 hover:bg-gray-50 cursor-pointer">
<input type="checkbox" checked={form.ret_pricing} onChange={(e) => updateForm({ ret_pricing: e.target.checked })} className="w-4 h-4 rounded border-gray-300 text-purple-600 focus:ring-purple-500" />
<div><span className="text-sm font-medium text-gray-900">Personalisierte Preisgestaltung</span><p className="text-xs text-gray-500">Individuelle Preise basierend auf Nutzerprofil</p></div>
</label>
<label className="flex items-center gap-3 p-3 rounded-lg border border-gray-200 hover:bg-gray-50 cursor-pointer">
<input type="checkbox" checked={form.ret_credit_scoring} onChange={(e) => updateForm({ ret_credit_scoring: e.target.checked })} className="w-4 h-4 rounded border-gray-300 text-purple-600 focus:ring-purple-500" />
<div><span className="text-sm font-medium text-gray-900">Bonitaetspruefung bei Kauf auf Rechnung</span><p className="text-xs text-gray-500">Kredit-Scoring beeinflusst Zugang zu Zahlungsarten</p></div>
</label>
<label className="flex items-center gap-3 p-3 rounded-lg border border-gray-200 hover:bg-gray-50 cursor-pointer">
<input type="checkbox" checked={form.ret_dark_patterns} onChange={(e) => updateForm({ ret_dark_patterns: e.target.checked })} className="w-4 h-4 rounded border-gray-300 text-purple-600 focus:ring-purple-500" />
<div><span className="text-sm font-medium text-gray-900">Manipulative UI-Muster moeglich (Dark Patterns)</span><p className="text-xs text-gray-500">Kuenstliche Verknappung, Social Proof, versteckte Kosten</p></div>
</label>
</div>
</div>
)}
{/* IT / Cybersecurity / Telecom */}
{['it_services', 'cybersecurity', 'telecom'].includes(form.domain) && (
<div className="mt-6 pt-6 border-t border-gray-200">
<h3 className="text-sm font-semibold text-gray-900 mb-1">IT & Cybersecurity Compliance-Fragen</h3>
<p className="text-xs text-gray-500 mb-4">NIS2, DSGVO, BetrVG §87.</p>
<div className="space-y-3">
<label className="flex items-center gap-3 p-3 rounded-lg border border-gray-200 hover:bg-gray-50 cursor-pointer">
<input type="checkbox" checked={form.its_surveillance} onChange={(e) => updateForm({ its_surveillance: e.target.checked })} className="w-4 h-4 rounded border-gray-300 text-purple-600 focus:ring-purple-500" />
<div><span className="text-sm font-medium text-gray-900">Mitarbeiterueberwachung (SIEM, DLP, UBA)</span><p className="text-xs text-gray-500">User Behavior Analytics, Data Loss Prevention mit Personenbezug</p></div>
</label>
<label className="flex items-center gap-3 p-3 rounded-lg border border-gray-200 hover:bg-gray-50 cursor-pointer">
<input type="checkbox" checked={form.its_threat_detection} onChange={(e) => updateForm({ its_threat_detection: e.target.checked })} className="w-4 h-4 rounded border-gray-300 text-purple-600 focus:ring-purple-500" />
<div><span className="text-sm font-medium text-gray-900">KI-gestuetzte Bedrohungserkennung</span><p className="text-xs text-gray-500">Anomalie-Erkennung, Intrusion Detection</p></div>
</label>
<label className="flex items-center gap-3 p-3 rounded-lg border border-gray-200 hover:bg-gray-50 cursor-pointer">
<input type="checkbox" checked={form.its_data_retention} onChange={(e) => updateForm({ its_data_retention: e.target.checked })} className="w-4 h-4 rounded border-gray-300 text-purple-600 focus:ring-purple-500" />
<div><span className="text-sm font-medium text-gray-900">Umfangreiche Log-Speicherung</span><p className="text-xs text-gray-500">Security-Logs mit Personenbezug werden langfristig gespeichert</p></div>
</label>
</div>
</div>
)}
{/* Logistics */}
{['logistics'].includes(form.domain) && (
<div className="mt-6 pt-6 border-t border-gray-200">
<h3 className="text-sm font-semibold text-gray-900 mb-1">Logistik Compliance-Fragen</h3>
<p className="text-xs text-gray-500 mb-4">BetrVG §87, DSGVO Worker Tracking.</p>
<div className="space-y-3">
<label className="flex items-center gap-3 p-3 rounded-lg border border-gray-200 hover:bg-gray-50 cursor-pointer">
<input type="checkbox" checked={form.log_driver_tracking} onChange={(e) => updateForm({ log_driver_tracking: e.target.checked })} className="w-4 h-4 rounded border-gray-300 text-purple-600 focus:ring-purple-500" />
<div><span className="text-sm font-medium text-gray-900">Fahrer-/Kurier-Tracking (GPS)</span><p className="text-xs text-gray-500">Standortverfolgung von Mitarbeitern</p></div>
</label>
<label className="flex items-center gap-3 p-3 rounded-lg border border-gray-200 hover:bg-gray-50 cursor-pointer">
<input type="checkbox" checked={form.log_workload_scoring} onChange={(e) => updateForm({ log_workload_scoring: e.target.checked })} className="w-4 h-4 rounded border-gray-300 text-purple-600 focus:ring-purple-500" />
<div><span className="text-sm font-medium text-gray-900">Leistungsbewertung von Lager-/Liefermitarbeitern</span><p className="text-xs text-gray-500">Picks/Stunde, Liefergeschwindigkeit, Performance-Scores</p></div>
</label>
</div>
</div>
)}
{/* Construction / Real Estate */}
{['construction', 'real_estate', 'facility_management'].includes(form.domain) && (
<div className="mt-6 pt-6 border-t border-gray-200">
<h3 className="text-sm font-semibold text-gray-900 mb-1">Bau & Immobilien Compliance-Fragen</h3>
<p className="text-xs text-gray-500 mb-4">AGG, DSGVO, Arbeitsschutz.</p>
<div className="space-y-3">
<label className="flex items-center gap-3 p-3 rounded-lg border border-gray-200 hover:bg-gray-50 cursor-pointer">
<input type="checkbox" checked={form.con_tenant_screening} onChange={(e) => updateForm({ con_tenant_screening: e.target.checked })} className="w-4 h-4 rounded border-gray-300 text-purple-600 focus:ring-purple-500" />
<div><span className="text-sm font-medium text-gray-900">KI-gestuetzte Mieterauswahl</span><p className="text-xs text-gray-500">Bonitaetspruefung, Bewerber-Ranking fuer Wohnungen</p></div>
</label>
<label className="flex items-center gap-3 p-3 rounded-lg border border-gray-200 hover:bg-gray-50 cursor-pointer">
<input type="checkbox" checked={form.con_worker_safety} onChange={(e) => updateForm({ con_worker_safety: e.target.checked })} className="w-4 h-4 rounded border-gray-300 text-purple-600 focus:ring-purple-500" />
<div><span className="text-sm font-medium text-gray-900">KI-Arbeitsschutzueberwachung auf Baustellen</span><p className="text-xs text-gray-500">Kamera-basierte Sicherheitsueberwachung, Helm-Erkennung</p></div>
</label>
</div>
</div>
)}
{/* Marketing / Media */}
{['marketing', 'media', 'entertainment'].includes(form.domain) && (
<div className="mt-6 pt-6 border-t border-gray-200">
<h3 className="text-sm font-semibold text-gray-900 mb-1">Marketing & Medien Compliance-Fragen</h3>
<p className="text-xs text-gray-500 mb-4">Art. 50 AI Act (Deepfakes), DSA, DSGVO.</p>
<div className="space-y-3">
<label className="flex items-center gap-3 p-3 rounded-lg border border-red-200 bg-red-50 hover:bg-red-100 cursor-pointer">
<input type="checkbox" checked={form.mkt_deepfake} onChange={(e) => updateForm({ mkt_deepfake: e.target.checked })} className="w-4 h-4 rounded border-red-300 text-red-600 focus:ring-red-500" />
<div><span className="text-sm font-medium text-red-900">Synthetische Inhalte (Deepfakes)</span><p className="text-xs text-red-700">KI-generierte Bilder, Videos oder Stimmen Kennzeichnungspflicht!</p></div>
</label>
<label className="flex items-center gap-3 p-3 rounded-lg border border-gray-200 hover:bg-gray-50 cursor-pointer">
<input type="checkbox" checked={form.mkt_targeting} onChange={(e) => updateForm({ mkt_targeting: e.target.checked })} className="w-4 h-4 rounded border-gray-300 text-purple-600 focus:ring-purple-500" />
<div><span className="text-sm font-medium text-gray-900">Verhaltensbasiertes Targeting</span><p className="text-xs text-gray-500">Personalisierte Werbung basierend auf Nutzerverhalten</p></div>
</label>
<label className="flex items-center gap-3 p-3 rounded-lg border border-red-200 bg-red-50 hover:bg-red-100 cursor-pointer">
<input type="checkbox" checked={form.mkt_minors} onChange={(e) => updateForm({ mkt_minors: e.target.checked })} className="w-4 h-4 rounded border-red-300 text-red-600 focus:ring-red-500" />
<div><span className="text-sm font-medium text-red-900">Minderjaehrige als Zielgruppe</span><p className="text-xs text-red-700">Besonderer Schutz DSA Art. 28 verbietet Profiling Minderjaehriger</p></div>
</label>
<label className="flex items-center gap-3 p-3 rounded-lg border border-green-200 bg-green-50 hover:bg-green-100 cursor-pointer">
<input type="checkbox" checked={form.mkt_labeled} onChange={(e) => updateForm({ mkt_labeled: e.target.checked })} className="w-4 h-4 rounded border-green-300 text-green-600 focus:ring-green-500" />
<div><span className="text-sm font-medium text-green-900">KI-Inhalte werden als solche gekennzeichnet</span><p className="text-xs text-green-700">Art. 50 AI Act: Pflicht zur Kennzeichnung synthetischer Inhalte</p></div>
</label>
</div>
</div>
)}
{/* Manufacturing */}
{['mechanical_engineering', 'electrical_engineering', 'plant_engineering', 'chemicals', 'food_beverage', 'textiles', 'packaging'].includes(form.domain) && (
<div className="mt-6 pt-6 border-t border-gray-200">
<h3 className="text-sm font-semibold text-gray-900 mb-1">Fertigung Compliance-Fragen</h3>
<p className="text-xs text-gray-500 mb-4">Maschinenverordnung (EU) 2023/1230, CE-Kennzeichnung.</p>
<div className="space-y-3">
<label className="flex items-center gap-3 p-3 rounded-lg border border-red-200 bg-red-50 hover:bg-red-100 cursor-pointer">
<input type="checkbox" checked={form.mfg_machine_safety} onChange={(e) => updateForm({ mfg_machine_safety: e.target.checked })} className="w-4 h-4 rounded border-red-300 text-red-600 focus:ring-red-500" />
<div><span className="text-sm font-medium text-red-900">KI in Maschinensicherheit</span><p className="text-xs text-red-700">Sicherheitsrelevante Steuerung Validierung erforderlich</p></div>
</label>
<label className="flex items-center gap-3 p-3 rounded-lg border border-gray-200 hover:bg-gray-50 cursor-pointer">
<input type="checkbox" checked={form.mfg_ce_required} onChange={(e) => updateForm({ mfg_ce_required: e.target.checked })} className="w-4 h-4 rounded border-gray-300 text-purple-600 focus:ring-purple-500" />
<div><span className="text-sm font-medium text-gray-900">CE-Kennzeichnung erforderlich</span><p className="text-xs text-gray-500">Maschinenverordnung (EU) 2023/1230</p></div>
</label>
<label className="flex items-center gap-3 p-3 rounded-lg border border-green-200 bg-green-50 hover:bg-green-100 cursor-pointer">
<input type="checkbox" checked={form.mfg_validated} onChange={(e) => updateForm({ mfg_validated: e.target.checked })} className="w-4 h-4 rounded border-green-300 text-green-600 focus:ring-green-500" />
<div><span className="text-sm font-medium text-green-900">Sicherheitsvalidierung durchgefuehrt</span><p className="text-xs text-green-700">Konformitaetsbewertung nach Maschinenverordnung abgeschlossen</p></div>
</label>
</div>
</div>
)}
{/* Agriculture */}
{['agriculture', 'forestry', 'fishing'].includes(form.domain) && (
<div className="mt-6 pt-6 border-t border-gray-200">
<h3 className="text-sm font-semibold text-gray-900 mb-1">Landwirtschaft Compliance-Fragen</h3>
<div className="space-y-3">
<label className="flex items-center gap-3 p-3 rounded-lg border border-gray-200 hover:bg-gray-50 cursor-pointer">
<input type="checkbox" checked={form.agr_pesticide} onChange={(e) => updateForm({ agr_pesticide: e.target.checked })} className="w-4 h-4 rounded border-gray-300 text-purple-600 focus:ring-purple-500" />
<div><span className="text-sm font-medium text-gray-900">KI steuert Pestizideinsatz</span><p className="text-xs text-gray-500">Precision Farming, automatisierte Ausbringung</p></div>
</label>
<label className="flex items-center gap-3 p-3 rounded-lg border border-gray-200 hover:bg-gray-50 cursor-pointer">
<input type="checkbox" checked={form.agr_animal_welfare} onChange={(e) => updateForm({ agr_animal_welfare: e.target.checked })} className="w-4 h-4 rounded border-gray-300 text-purple-600 focus:ring-purple-500" />
<div><span className="text-sm font-medium text-gray-900">KI beeinflusst Tierhaltungsentscheidungen</span><p className="text-xs text-gray-500">Fuetterung, Gesundheit, Stallmanagement</p></div>
</label>
<label className="flex items-center gap-3 p-3 rounded-lg border border-gray-200 hover:bg-gray-50 cursor-pointer">
<input type="checkbox" checked={form.agr_environmental} onChange={(e) => updateForm({ agr_environmental: e.target.checked })} className="w-4 h-4 rounded border-gray-300 text-purple-600 focus:ring-purple-500" />
<div><span className="text-sm font-medium text-gray-900">Umweltdaten werden verarbeitet</span><p className="text-xs text-gray-500">Boden, Wasser, Emissionen</p></div>
</label>
</div>
</div>
)}
{/* Social Services */}
{['social_services', 'nonprofit'].includes(form.domain) && (
<div className="mt-6 pt-6 border-t border-gray-200">
<h3 className="text-sm font-semibold text-gray-900 mb-1">Soziales Compliance-Fragen</h3>
<div className="space-y-3">
<label className="flex items-center gap-3 p-3 rounded-lg border border-red-200 bg-red-50 hover:bg-red-100 cursor-pointer">
<input type="checkbox" checked={form.soc_vulnerable} onChange={(e) => updateForm({ soc_vulnerable: e.target.checked })} className="w-4 h-4 rounded border-red-300 text-red-600 focus:ring-red-500" />
<div><span className="text-sm font-medium text-red-900">Schutzbeduerftiger Personenkreis betroffen</span><p className="text-xs text-red-700">Kinder, Senioren, Gefluechtete, Menschen mit Behinderung</p></div>
</label>
<label className="flex items-center gap-3 p-3 rounded-lg border border-gray-200 hover:bg-gray-50 cursor-pointer">
<input type="checkbox" checked={form.soc_benefit} onChange={(e) => updateForm({ soc_benefit: e.target.checked })} className="w-4 h-4 rounded border-gray-300 text-purple-600 focus:ring-purple-500" />
<div><span className="text-sm font-medium text-gray-900">KI beeinflusst Leistungszuteilung</span><p className="text-xs text-gray-500">Sozialleistungen, Hilfsangebote, Foerderung</p></div>
</label>
<label className="flex items-center gap-3 p-3 rounded-lg border border-gray-200 hover:bg-gray-50 cursor-pointer">
<input type="checkbox" checked={form.soc_case_mgmt} onChange={(e) => updateForm({ soc_case_mgmt: e.target.checked })} className="w-4 h-4 rounded border-gray-300 text-purple-600 focus:ring-purple-500" />
<div><span className="text-sm font-medium text-gray-900">KI in Fallmanagement</span><p className="text-xs text-gray-500">Priorisierung, Zuordnung, Verlaufsprognose</p></div>
</label>
</div>
</div>
)}
{/* Hospitality / Tourism */}
{['hospitality', 'tourism'].includes(form.domain) && (
<div className="mt-6 pt-6 border-t border-gray-200">
<h3 className="text-sm font-semibold text-gray-900 mb-1">Tourismus & Gastronomie Compliance-Fragen</h3>
<div className="space-y-3">
<label className="flex items-center gap-3 p-3 rounded-lg border border-gray-200 hover:bg-gray-50 cursor-pointer">
<input type="checkbox" checked={form.hos_guest_profiling} onChange={(e) => updateForm({ hos_guest_profiling: e.target.checked })} className="w-4 h-4 rounded border-gray-300 text-purple-600 focus:ring-purple-500" />
<div><span className="text-sm font-medium text-gray-900">Gaeste-Profilbildung</span><p className="text-xs text-gray-500">Praeferenzen, Buchungsverhalten, Segmentierung</p></div>
</label>
<label className="flex items-center gap-3 p-3 rounded-lg border border-gray-200 hover:bg-gray-50 cursor-pointer">
<input type="checkbox" checked={form.hos_dynamic_pricing} onChange={(e) => updateForm({ hos_dynamic_pricing: e.target.checked })} className="w-4 h-4 rounded border-gray-300 text-purple-600 focus:ring-purple-500" />
<div><span className="text-sm font-medium text-gray-900">Dynamische Preisgestaltung</span><p className="text-xs text-gray-500">Personalisierte Zimmer-/Flugreise</p></div>
</label>
<label className="flex items-center gap-3 p-3 rounded-lg border border-red-200 bg-red-50 hover:bg-red-100 cursor-pointer">
<input type="checkbox" checked={form.hos_review_manipulation} onChange={(e) => updateForm({ hos_review_manipulation: e.target.checked })} className="w-4 h-4 rounded border-red-300 text-red-600 focus:ring-red-500" />
<div><span className="text-sm font-medium text-red-900">KI manipuliert oder generiert Bewertungen</span><p className="text-xs text-red-700">Fake Reviews sind unzulaessig (UWG, DSA)</p></div>
</label>
</div>
</div>
)}
{/* Insurance */}
{['insurance'].includes(form.domain) && (
<div className="mt-6 pt-6 border-t border-gray-200">
<h3 className="text-sm font-semibold text-gray-900 mb-1">Versicherung Compliance-Fragen</h3>
<div className="space-y-3">
<label className="flex items-center gap-3 p-3 rounded-lg border border-gray-200 hover:bg-gray-50 cursor-pointer">
<input type="checkbox" checked={form.ins_premium} onChange={(e) => updateForm({ ins_premium: e.target.checked })} className="w-4 h-4 rounded border-gray-300 text-purple-600 focus:ring-purple-500" />
<div><span className="text-sm font-medium text-gray-900">KI berechnet individuelle Praemien</span><p className="text-xs text-gray-500">Risikoadjustierte Preisgestaltung</p></div>
</label>
<label className="flex items-center gap-3 p-3 rounded-lg border border-gray-200 hover:bg-gray-50 cursor-pointer">
<input type="checkbox" checked={form.ins_claims} onChange={(e) => updateForm({ ins_claims: e.target.checked })} className="w-4 h-4 rounded border-gray-300 text-purple-600 focus:ring-purple-500" />
<div><span className="text-sm font-medium text-gray-900">Automatisierte Schadenbearbeitung</span><p className="text-xs text-gray-500">KI entscheidet ueber Schadenregulierung</p></div>
</label>
<label className="flex items-center gap-3 p-3 rounded-lg border border-gray-200 hover:bg-gray-50 cursor-pointer">
<input type="checkbox" checked={form.ins_fraud} onChange={(e) => updateForm({ ins_fraud: e.target.checked })} className="w-4 h-4 rounded border-gray-300 text-purple-600 focus:ring-purple-500" />
<div><span className="text-sm font-medium text-gray-900">KI-Betrugserkennung</span><p className="text-xs text-gray-500">Automatische Verdachtsfallerkennung</p></div>
</label>
</div>
</div>
)}
{/* Investment */}
{['investment'].includes(form.domain) && (
<div className="mt-6 pt-6 border-t border-gray-200">
<h3 className="text-sm font-semibold text-gray-900 mb-1">Investment Compliance-Fragen</h3>
<div className="space-y-3">
<label className="flex items-center gap-3 p-3 rounded-lg border border-gray-200 hover:bg-gray-50 cursor-pointer">
<input type="checkbox" checked={form.inv_algo_trading} onChange={(e) => updateForm({ inv_algo_trading: e.target.checked })} className="w-4 h-4 rounded border-gray-300 text-purple-600 focus:ring-purple-500" />
<div><span className="text-sm font-medium text-gray-900">Algorithmischer Handel</span><p className="text-xs text-gray-500">Automated Trading, HFT MiFID II relevant</p></div>
</label>
<label className="flex items-center gap-3 p-3 rounded-lg border border-gray-200 hover:bg-gray-50 cursor-pointer">
<input type="checkbox" checked={form.inv_robo} onChange={(e) => updateForm({ inv_robo: e.target.checked })} className="w-4 h-4 rounded border-gray-300 text-purple-600 focus:ring-purple-500" />
<div><span className="text-sm font-medium text-gray-900">Robo Advisor / KI-Anlageberatung</span><p className="text-xs text-gray-500">Automatisierte Vermoegensberatung WpHG-Pflichten</p></div>
</label>
</div>
</div>
)}
{/* Defense */}
{['defense'].includes(form.domain) && (
<div className="mt-6 pt-6 border-t border-gray-200">
<h3 className="text-sm font-semibold text-gray-900 mb-1">Verteidigung Compliance-Fragen</h3>
<div className="space-y-3">
<label className="flex items-center gap-3 p-3 rounded-lg border border-red-200 bg-red-50 hover:bg-red-100 cursor-pointer">
<input type="checkbox" checked={form.def_dual_use} onChange={(e) => updateForm({ def_dual_use: e.target.checked })} className="w-4 h-4 rounded border-red-300 text-red-600 focus:ring-red-500" />
<div><span className="text-sm font-medium text-red-900">Dual-Use KI-Technologie</span><p className="text-xs text-red-700">Exportkontrolle (EU VO 2021/821) beachten</p></div>
</label>
<label className="flex items-center gap-3 p-3 rounded-lg border border-gray-200 hover:bg-gray-50 cursor-pointer">
<input type="checkbox" checked={form.def_classified} onChange={(e) => updateForm({ def_classified: e.target.checked })} className="w-4 h-4 rounded border-gray-300 text-purple-600 focus:ring-purple-500" />
<div><span className="text-sm font-medium text-gray-900">Verschlusssachen werden verarbeitet</span><p className="text-xs text-gray-500">VS-NfD oder hoeher besondere Schutzmassnahmen</p></div>
</label>
</div>
</div>
)}
{/* Supply Chain (Textiles, Packaging) */}
{['textiles', 'packaging'].includes(form.domain) && (
<div className="mt-6 pt-6 border-t border-gray-200">
<h3 className="text-sm font-semibold text-gray-900 mb-1">Lieferkette Compliance-Fragen</h3>
<p className="text-xs text-gray-500 mb-4">LkSG Lieferkettensorgfaltspflichtengesetz.</p>
<div className="space-y-3">
<label className="flex items-center gap-3 p-3 rounded-lg border border-gray-200 hover:bg-gray-50 cursor-pointer">
<input type="checkbox" checked={form.sch_supplier} onChange={(e) => updateForm({ sch_supplier: e.target.checked })} className="w-4 h-4 rounded border-gray-300 text-purple-600 focus:ring-purple-500" />
<div><span className="text-sm font-medium text-gray-900">KI ueberwacht Lieferanten</span><p className="text-xs text-gray-500">Lieferantenbewertung, Risikoanalyse</p></div>
</label>
<label className="flex items-center gap-3 p-3 rounded-lg border border-gray-200 hover:bg-gray-50 cursor-pointer">
<input type="checkbox" checked={form.sch_human_rights} onChange={(e) => updateForm({ sch_human_rights: e.target.checked })} className="w-4 h-4 rounded border-gray-300 text-purple-600 focus:ring-purple-500" />
<div><span className="text-sm font-medium text-gray-900">KI prueft Menschenrechte in Lieferkette</span><p className="text-xs text-gray-500">LkSG-Sorgfaltspflichten, Kinderarbeit, Zwangsarbeit</p></div>
</label>
</div>
</div>
)}
{/* Sports */}
{['sports'].includes(form.domain) && (
<div className="mt-6 pt-6 border-t border-gray-200">
<h3 className="text-sm font-semibold text-gray-900 mb-1">Sport Compliance-Fragen</h3>
<div className="space-y-3">
<label className="flex items-center gap-3 p-3 rounded-lg border border-gray-200 hover:bg-gray-50 cursor-pointer">
<input type="checkbox" checked={form.spo_athlete} onChange={(e) => updateForm({ spo_athlete: e.target.checked })} className="w-4 h-4 rounded border-gray-300 text-purple-600 focus:ring-purple-500" />
<div><span className="text-sm font-medium text-gray-900">Athleten-Performance-Tracking</span><p className="text-xs text-gray-500">GPS, Biometrie, Leistungsdaten</p></div>
</label>
<label className="flex items-center gap-3 p-3 rounded-lg border border-gray-200 hover:bg-gray-50 cursor-pointer">
<input type="checkbox" checked={form.spo_fan} onChange={(e) => updateForm({ spo_fan: e.target.checked })} className="w-4 h-4 rounded border-gray-300 text-purple-600 focus:ring-purple-500" />
<div><span className="text-sm font-medium text-gray-900">Fan-/Zuschauer-Profilbildung</span><p className="text-xs text-gray-500">Ticketing, Merchandising, Stadion-Tracking</p></div>
</label>
</div>
</div>
)}
{/* Finance / Banking */}
{['finance', 'banking'].includes(form.domain) && (
<div className="mt-6 pt-6 border-t border-gray-200">
<h3 className="text-sm font-semibold text-gray-900 mb-1">Finanzdienstleistungen Compliance-Fragen</h3>
<p className="text-xs text-gray-500 mb-4">DORA, MaRisk, BAIT, AI Act Annex III Nr. 5.</p>
<div className="space-y-3">
<label className="flex items-center gap-3 p-3 rounded-lg border border-gray-200 hover:bg-gray-50 cursor-pointer">
<input type="checkbox" checked={form.fin_credit_scoring} onChange={(e) => updateForm({ fin_credit_scoring: e.target.checked })} className="w-4 h-4 rounded border-gray-300 text-purple-600 focus:ring-purple-500" />
<div><span className="text-sm font-medium text-gray-900">KI-gestuetztes Kredit-Scoring</span><p className="text-xs text-gray-500">Bonitaetsbewertung, Kreditwuerdigkeitspruefung Art. 22 DSGVO + AGG</p></div>
</label>
<label className="flex items-center gap-3 p-3 rounded-lg border border-gray-200 hover:bg-gray-50 cursor-pointer">
<input type="checkbox" checked={form.fin_aml_kyc} onChange={(e) => updateForm({ fin_aml_kyc: e.target.checked })} className="w-4 h-4 rounded border-gray-300 text-purple-600 focus:ring-purple-500" />
<div><span className="text-sm font-medium text-gray-900">AML/KYC Automatisierung</span><p className="text-xs text-gray-500">Geldwaeschebekacmpfung, Kundenidentifizierung durch KI</p></div>
</label>
<label className="flex items-center gap-3 p-3 rounded-lg border border-gray-200 hover:bg-gray-50 cursor-pointer">
<input type="checkbox" checked={form.fin_algo_decisions} onChange={(e) => updateForm({ fin_algo_decisions: e.target.checked })} className="w-4 h-4 rounded border-gray-300 text-purple-600 focus:ring-purple-500" />
<div><span className="text-sm font-medium text-gray-900">Automatisierte Finanzentscheidungen</span><p className="text-xs text-gray-500">Kreditvergabe, Kontosperrung, Limitaenderungen</p></div>
</label>
<label className="flex items-center gap-3 p-3 rounded-lg border border-gray-200 hover:bg-gray-50 cursor-pointer">
<input type="checkbox" checked={form.fin_customer_profiling} onChange={(e) => updateForm({ fin_customer_profiling: e.target.checked })} className="w-4 h-4 rounded border-gray-300 text-purple-600 focus:ring-purple-500" />
<div><span className="text-sm font-medium text-gray-900">Kunden-Profilbildung / Segmentierung</span><p className="text-xs text-gray-500">Risikoklassifikation, Produkt-Empfehlungen</p></div>
</label>
</div>
</div>
)}
{/* General — universal AI governance questions */}
{form.domain === 'general' && (
<div className="mt-6 pt-6 border-t border-gray-200">
<h3 className="text-sm font-semibold text-gray-900 mb-1">Allgemeine KI-Governance</h3>
<p className="text-xs text-gray-500 mb-4">Grundlegende Compliance-Fragen fuer jeden KI-Einsatz.</p>
<div className="space-y-3">
<label className="flex items-center gap-3 p-3 rounded-lg border border-gray-200 hover:bg-gray-50 cursor-pointer">
<input type="checkbox" checked={form.gen_affects_people} onChange={(e) => updateForm({ gen_affects_people: e.target.checked })} className="w-4 h-4 rounded border-gray-300 text-purple-600 focus:ring-purple-500" />
<div><span className="text-sm font-medium text-gray-900">System hat Auswirkungen auf natuerliche Personen</span><p className="text-xs text-gray-500">Entscheidungen, Empfehlungen oder Bewertungen betreffen Menschen direkt</p></div>
</label>
<label className="flex items-center gap-3 p-3 rounded-lg border border-gray-200 hover:bg-gray-50 cursor-pointer">
<input type="checkbox" checked={form.gen_automated_decisions} onChange={(e) => updateForm({ gen_automated_decisions: e.target.checked })} className="w-4 h-4 rounded border-gray-300 text-purple-600 focus:ring-purple-500" />
<div><span className="text-sm font-medium text-gray-900">Automatisierte Entscheidungen werden getroffen</span><p className="text-xs text-gray-500">KI trifft oder beeinflusst Entscheidungen ohne menschliche Pruefung</p></div>
</label>
<label className="flex items-center gap-3 p-3 rounded-lg border border-gray-200 hover:bg-gray-50 cursor-pointer">
<input type="checkbox" checked={form.gen_sensitive_data} onChange={(e) => updateForm({ gen_sensitive_data: e.target.checked })} className="w-4 h-4 rounded border-gray-300 text-purple-600 focus:ring-purple-500" />
<div><span className="text-sm font-medium text-gray-900">Sensible oder vertrauliche Daten verarbeitet</span><p className="text-xs text-gray-500">Geschaeftsgeheimnisse, personenbezogene Daten, vertrauliche Informationen</p></div>
</label>
</div>
</div>
)}
</div>
)}

View File

@@ -3,6 +3,7 @@
import React, { useState, useEffect } from 'react'
import { useSDK } from '@/lib/sdk'
import { StepHeader, STEP_EXPLANATIONS } from '@/components/sdk/StepHeader'
import DecisionTreeWizard from '@/components/sdk/ai-act/DecisionTreeWizard'
// =============================================================================
// TYPES
@@ -21,6 +22,8 @@ interface AISystem {
assessmentResult: Record<string, unknown> | null
}
type TabId = 'overview' | 'decision-tree' | 'results'
// =============================================================================
// LOADING SKELETON
// =============================================================================
@@ -306,12 +309,178 @@ function AISystemCard({
)
}
// =============================================================================
// SAVED RESULTS TAB
// =============================================================================
interface SavedResult {
id: string
system_name: string
system_description?: string
high_risk_result: string
gpai_result: { gpai_category: string; is_systemic_risk: boolean }
combined_obligations: string[]
created_at: string
}
function SavedResultsTab() {
const [results, setResults] = useState<SavedResult[]>([])
const [loading, setLoading] = useState(true)
useEffect(() => {
const load = async () => {
try {
const res = await fetch('/api/sdk/v1/ucca/decision-tree/results')
if (res.ok) {
const data = await res.json()
setResults(data.results || [])
}
} catch {
// Ignore
} finally {
setLoading(false)
}
}
load()
}, [])
const handleDelete = async (id: string) => {
if (!confirm('Ergebnis wirklich löschen?')) return
try {
const res = await fetch(`/api/sdk/v1/ucca/decision-tree/results/${id}`, { method: 'DELETE' })
if (res.ok) {
setResults(prev => prev.filter(r => r.id !== id))
}
} catch {
// Ignore
}
}
const riskLabels: Record<string, string> = {
unacceptable: 'Unzulässig',
high_risk: 'Hochrisiko',
limited_risk: 'Begrenztes Risiko',
minimal_risk: 'Minimales Risiko',
not_applicable: 'Nicht anwendbar',
}
const riskColors: Record<string, string> = {
unacceptable: 'bg-red-100 text-red-700',
high_risk: 'bg-orange-100 text-orange-700',
limited_risk: 'bg-yellow-100 text-yellow-700',
minimal_risk: 'bg-green-100 text-green-700',
not_applicable: 'bg-gray-100 text-gray-500',
}
const gpaiLabels: Record<string, string> = {
none: 'Kein GPAI',
standard: 'GPAI Standard',
systemic: 'GPAI Systemisch',
}
const gpaiColors: Record<string, string> = {
none: 'bg-gray-100 text-gray-500',
standard: 'bg-blue-100 text-blue-700',
systemic: 'bg-purple-100 text-purple-700',
}
if (loading) {
return <LoadingSkeleton />
}
if (results.length === 0) {
return (
<div className="bg-white rounded-xl border border-gray-200 p-12 text-center">
<div className="w-16 h-16 mx-auto bg-purple-100 rounded-full flex items-center justify-center mb-4">
<svg className="w-8 h-8 text-purple-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2" />
</svg>
</div>
<h3 className="text-lg font-semibold text-gray-900">Keine Ergebnisse vorhanden</h3>
<p className="mt-2 text-gray-500">Nutzen Sie den Entscheidungsbaum, um KI-Systeme zu klassifizieren.</p>
</div>
)
}
return (
<div className="space-y-4">
{results.map(r => (
<div key={r.id} className="bg-white rounded-xl border border-gray-200 p-5">
<div className="flex items-start justify-between">
<div>
<h4 className="font-semibold text-gray-900">{r.system_name}</h4>
{r.system_description && (
<p className="text-sm text-gray-500 mt-0.5">{r.system_description}</p>
)}
<div className="flex items-center gap-2 mt-2">
<span className={`px-2 py-1 text-xs rounded-full ${riskColors[r.high_risk_result] || 'bg-gray-100 text-gray-500'}`}>
{riskLabels[r.high_risk_result] || r.high_risk_result}
</span>
<span className={`px-2 py-1 text-xs rounded-full ${gpaiColors[r.gpai_result?.gpai_category] || 'bg-gray-100 text-gray-500'}`}>
{gpaiLabels[r.gpai_result?.gpai_category] || 'Kein GPAI'}
</span>
{r.gpai_result?.is_systemic_risk && (
<span className="px-2 py-1 text-xs rounded-full bg-red-100 text-red-700">Systemisch</span>
)}
</div>
<div className="text-xs text-gray-400 mt-2">
{r.combined_obligations?.length || 0} Pflichten &middot; {new Date(r.created_at).toLocaleDateString('de-DE')}
</div>
</div>
<button
onClick={() => handleDelete(r.id)}
className="px-3 py-1 text-xs text-red-600 hover:bg-red-50 rounded transition-colors"
>
Löschen
</button>
</div>
</div>
))}
</div>
)
}
// =============================================================================
// TABS
// =============================================================================
const TABS: { id: TabId; label: string; icon: React.ReactNode }[] = [
{
id: 'overview',
label: 'Übersicht',
icon: (
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M3.75 6A2.25 2.25 0 016 3.75h2.25A2.25 2.25 0 0110.5 6v2.25a2.25 2.25 0 01-2.25 2.25H6a2.25 2.25 0 01-2.25-2.25V6zM3.75 15.75A2.25 2.25 0 016 13.5h2.25a2.25 2.25 0 012.25 2.25V18a2.25 2.25 0 01-2.25 2.25H6A2.25 2.25 0 013.75 18v-2.25zM13.5 6a2.25 2.25 0 012.25-2.25H18A2.25 2.25 0 0120.25 6v2.25A2.25 2.25 0 0118 10.5h-2.25a2.25 2.25 0 01-2.25-2.25V6zM13.5 15.75a2.25 2.25 0 012.25-2.25H18a2.25 2.25 0 012.25 2.25V18A2.25 2.25 0 0118 20.25h-2.25A2.25 2.25 0 0113.5 18v-2.25z" />
</svg>
),
},
{
id: 'decision-tree',
label: 'Entscheidungsbaum',
icon: (
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M3.75 12h16.5m-16.5 3.75h16.5M3.75 19.5h16.5M5.625 4.5h12.75a1.875 1.875 0 010 3.75H5.625a1.875 1.875 0 010-3.75z" />
</svg>
),
},
{
id: 'results',
label: 'Ergebnisse',
icon: (
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M9 12h3.75M9 15h3.75M9 18h3.75m3 .75H18a2.25 2.25 0 002.25-2.25V6.108c0-1.135-.845-2.098-1.976-2.192a48.424 48.424 0 00-1.123-.08m-5.801 0c-.065.21-.1.433-.1.664 0 .414.336.75.75.75h4.5a.75.75 0 00.75-.75 2.25 2.25 0 00-.1-.664m-5.8 0A2.251 2.251 0 0113.5 2.25H15c1.012 0 1.867.668 2.15 1.586m-5.8 0c-.376.023-.75.05-1.124.08C9.095 4.01 8.25 4.973 8.25 6.108V8.25m0 0H4.875c-.621 0-1.125.504-1.125 1.125v11.25c0 .621.504 1.125 1.125 1.125h9.75c.621 0 1.125-.504 1.125-1.125V9.375c0-.621-.504-1.125-1.125-1.125H8.25z" />
</svg>
),
},
]
// =============================================================================
// MAIN PAGE
// =============================================================================
export default function AIActPage() {
const { state } = useSDK()
const [activeTab, setActiveTab] = useState<TabId>('overview')
const [systems, setSystems] = useState<AISystem[]>([])
const [filter, setFilter] = useState<string>('all')
const [showAddForm, setShowAddForm] = useState(false)
@@ -354,7 +523,6 @@ export default function AIActPage() {
const handleAddSystem = async (data: Omit<AISystem, 'id' | 'assessmentDate' | 'assessmentResult'>) => {
setError(null)
if (editingSystem) {
// Edit existing system via PUT
try {
const res = await fetch(`/api/sdk/v1/compliance/ai/systems/${editingSystem.id}`, {
method: 'PUT',
@@ -380,14 +548,12 @@ export default function AIActPage() {
setError('Speichern fehlgeschlagen')
}
} catch {
// Fallback: update locally
setSystems(prev => prev.map(s =>
s.id === editingSystem.id ? { ...s, ...data } : s
))
}
setEditingSystem(null)
} else {
// Create new system via POST
try {
const res = await fetch('/api/sdk/v1/compliance/ai/systems', {
method: 'POST',
@@ -415,7 +581,6 @@ export default function AIActPage() {
setError('Registrierung fehlgeschlagen')
}
} catch {
// Fallback: add locally
const newSystem: AISystem = {
...data,
id: `ai-${Date.now()}`,
@@ -503,17 +668,37 @@ export default function AIActPage() {
explanation={stepInfo.explanation}
tips={stepInfo.tips}
>
<button
onClick={() => setShowAddForm(true)}
className="flex items-center gap-2 px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
</svg>
KI-System registrieren
</button>
{activeTab === 'overview' && (
<button
onClick={() => setShowAddForm(true)}
className="flex items-center gap-2 px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
</svg>
KI-System registrieren
</button>
)}
</StepHeader>
{/* Tabs */}
<div className="flex items-center gap-1 bg-gray-100 p-1 rounded-lg w-fit">
{TABS.map(tab => (
<button
key={tab.id}
onClick={() => setActiveTab(tab.id)}
className={`flex items-center gap-2 px-4 py-2 text-sm font-medium rounded-md transition-colors ${
activeTab === tab.id
? 'bg-white text-purple-700 shadow-sm'
: 'text-gray-600 hover:text-gray-900'
}`}
>
{tab.icon}
{tab.label}
</button>
))}
</div>
{/* Error Banner */}
{error && (
<div className="p-4 bg-red-50 border border-red-200 rounded-lg text-red-700 flex items-center justify-between">
@@ -522,90 +707,105 @@ export default function AIActPage() {
</div>
)}
{/* Add/Edit System Form */}
{showAddForm && (
<AddSystemForm
onSubmit={handleAddSystem}
onCancel={() => { setShowAddForm(false); setEditingSystem(null) }}
initialData={editingSystem}
/>
)}
{/* Stats */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<div className="bg-white rounded-xl border border-gray-200 p-6">
<div className="text-sm text-gray-500">KI-Systeme gesamt</div>
<div className="text-3xl font-bold text-gray-900">{systems.length}</div>
</div>
<div className="bg-white rounded-xl border border-orange-200 p-6">
<div className="text-sm text-orange-600">Hochrisiko</div>
<div className="text-3xl font-bold text-orange-600">{highRiskCount}</div>
</div>
<div className="bg-white rounded-xl border border-green-200 p-6">
<div className="text-sm text-green-600">Konform</div>
<div className="text-3xl font-bold text-green-600">{compliantCount}</div>
</div>
<div className="bg-white rounded-xl border border-gray-200 p-6">
<div className="text-sm text-gray-500">Nicht klassifiziert</div>
<div className="text-3xl font-bold text-gray-500">{unclassifiedCount}</div>
</div>
</div>
{/* Risk Pyramid */}
<RiskPyramid systems={systems} />
{/* Filter */}
<div className="flex items-center gap-2 flex-wrap">
<span className="text-sm text-gray-500">Filter:</span>
{['all', 'high-risk', 'limited-risk', 'minimal-risk', 'unclassified', 'compliant', 'non-compliant'].map(f => (
<button
key={f}
onClick={() => setFilter(f)}
className={`px-3 py-1 text-sm rounded-full transition-colors ${
filter === f
? 'bg-purple-600 text-white'
: 'bg-gray-100 text-gray-600 hover:bg-gray-200'
}`}
>
{f === 'all' ? 'Alle' :
f === 'high-risk' ? 'Hochrisiko' :
f === 'limited-risk' ? 'Begrenztes Risiko' :
f === 'minimal-risk' ? 'Minimales Risiko' :
f === 'unclassified' ? 'Nicht klassifiziert' :
f === 'compliant' ? 'Konform' : 'Nicht konform'}
</button>
))}
</div>
{/* Loading */}
{loading && <LoadingSkeleton />}
{/* AI Systems List */}
{!loading && (
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{filteredSystems.map(system => (
<AISystemCard
key={system.id}
system={system}
onAssess={() => handleAssess(system.id)}
onEdit={() => handleEdit(system)}
onDelete={() => handleDelete(system.id)}
assessing={assessingId === system.id}
{/* Tab: Overview */}
{activeTab === 'overview' && (
<>
{/* Add/Edit System Form */}
{showAddForm && (
<AddSystemForm
onSubmit={handleAddSystem}
onCancel={() => { setShowAddForm(false); setEditingSystem(null) }}
initialData={editingSystem}
/>
))}
</div>
)}
{/* Stats */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<div className="bg-white rounded-xl border border-gray-200 p-6">
<div className="text-sm text-gray-500">KI-Systeme gesamt</div>
<div className="text-3xl font-bold text-gray-900">{systems.length}</div>
</div>
<div className="bg-white rounded-xl border border-orange-200 p-6">
<div className="text-sm text-orange-600">Hochrisiko</div>
<div className="text-3xl font-bold text-orange-600">{highRiskCount}</div>
</div>
<div className="bg-white rounded-xl border border-green-200 p-6">
<div className="text-sm text-green-600">Konform</div>
<div className="text-3xl font-bold text-green-600">{compliantCount}</div>
</div>
<div className="bg-white rounded-xl border border-gray-200 p-6">
<div className="text-sm text-gray-500">Nicht klassifiziert</div>
<div className="text-3xl font-bold text-gray-500">{unclassifiedCount}</div>
</div>
</div>
{/* Risk Pyramid */}
<RiskPyramid systems={systems} />
{/* Filter */}
<div className="flex items-center gap-2 flex-wrap">
<span className="text-sm text-gray-500">Filter:</span>
{['all', 'high-risk', 'limited-risk', 'minimal-risk', 'unclassified', 'compliant', 'non-compliant'].map(f => (
<button
key={f}
onClick={() => setFilter(f)}
className={`px-3 py-1 text-sm rounded-full transition-colors ${
filter === f
? 'bg-purple-600 text-white'
: 'bg-gray-100 text-gray-600 hover:bg-gray-200'
}`}
>
{f === 'all' ? 'Alle' :
f === 'high-risk' ? 'Hochrisiko' :
f === 'limited-risk' ? 'Begrenztes Risiko' :
f === 'minimal-risk' ? 'Minimales Risiko' :
f === 'unclassified' ? 'Nicht klassifiziert' :
f === 'compliant' ? 'Konform' : 'Nicht konform'}
</button>
))}
</div>
{/* Loading */}
{loading && <LoadingSkeleton />}
{/* AI Systems List */}
{!loading && (
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{filteredSystems.map(system => (
<AISystemCard
key={system.id}
system={system}
onAssess={() => handleAssess(system.id)}
onEdit={() => handleEdit(system)}
onDelete={() => handleDelete(system.id)}
assessing={assessingId === system.id}
/>
))}
</div>
)}
{!loading && filteredSystems.length === 0 && (
<div className="bg-white rounded-xl border border-gray-200 p-12 text-center">
<div className="w-16 h-16 mx-auto bg-purple-100 rounded-full flex items-center justify-center mb-4">
<svg className="w-8 h-8 text-purple-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
</svg>
</div>
<h3 className="text-lg font-semibold text-gray-900">Keine KI-Systeme gefunden</h3>
<p className="mt-2 text-gray-500">Passen Sie den Filter an oder registrieren Sie ein neues KI-System.</p>
</div>
)}
</>
)}
{!loading && filteredSystems.length === 0 && (
<div className="bg-white rounded-xl border border-gray-200 p-12 text-center">
<div className="w-16 h-16 mx-auto bg-purple-100 rounded-full flex items-center justify-center mb-4">
<svg className="w-8 h-8 text-purple-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
</svg>
</div>
<h3 className="text-lg font-semibold text-gray-900">Keine KI-Systeme gefunden</h3>
<p className="mt-2 text-gray-500">Passen Sie den Filter an oder registrieren Sie ein neues KI-System.</p>
</div>
{/* Tab: Decision Tree */}
{activeTab === 'decision-tree' && (
<DecisionTreeWizard />
)}
{/* Tab: Results */}
{activeTab === 'results' && (
<SavedResultsTab />
)}
</div>
)

View File

@@ -0,0 +1,491 @@
'use client'
import React, { useState, useEffect } from 'react'
interface Registration {
id: string
system_name: string
system_version: string
risk_classification: string
gpai_classification: string
registration_status: string
eu_database_id: string
provider_name: string
created_at: string
}
const STATUS_STYLES: Record<string, { bg: string; text: string; label: string }> = {
draft: { bg: 'bg-gray-100', text: 'text-gray-700', label: 'Entwurf' },
ready: { bg: 'bg-blue-100', text: 'text-blue-700', label: 'Bereit' },
submitted: { bg: 'bg-yellow-100', text: 'text-yellow-700', label: 'Eingereicht' },
registered: { bg: 'bg-green-100', text: 'text-green-700', label: 'Registriert' },
update_required: { bg: 'bg-orange-100', text: 'text-orange-700', label: 'Update noetig' },
withdrawn: { bg: 'bg-red-100', text: 'text-red-700', label: 'Zurueckgezogen' },
}
const RISK_STYLES: Record<string, { bg: string; text: string }> = {
high_risk: { bg: 'bg-red-100', text: 'text-red-700' },
limited_risk: { bg: 'bg-yellow-100', text: 'text-yellow-700' },
minimal_risk: { bg: 'bg-green-100', text: 'text-green-700' },
not_classified: { bg: 'bg-gray-100', text: 'text-gray-500' },
}
const INITIAL_FORM = {
system_name: '',
system_version: '1.0',
system_description: '',
intended_purpose: '',
provider_name: '',
provider_legal_form: '',
provider_address: '',
provider_country: 'DE',
eu_representative_name: '',
eu_representative_contact: '',
risk_classification: 'not_classified',
annex_iii_category: '',
gpai_classification: 'none',
conformity_assessment_type: 'internal',
notified_body_name: '',
notified_body_id: '',
ce_marking: false,
training_data_summary: '',
}
export default function AIRegistrationPage() {
const [registrations, setRegistrations] = useState<Registration[]>([])
const [loading, setLoading] = useState(true)
const [showWizard, setShowWizard] = useState(false)
const [wizardStep, setWizardStep] = useState(1)
const [form, setForm] = useState({ ...INITIAL_FORM })
const [submitting, setSubmitting] = useState(false)
const [error, setError] = useState<string | null>(null)
useEffect(() => { loadRegistrations() }, [])
async function loadRegistrations() {
try {
setLoading(true)
const resp = await fetch('/api/sdk/v1/ai-registration')
if (resp.ok) {
const data = await resp.json()
setRegistrations(data.registrations || [])
}
} catch {
setError('Fehler beim Laden')
} finally {
setLoading(false)
}
}
async function handleSubmit() {
setSubmitting(true)
try {
const resp = await fetch('/api/sdk/v1/ai-registration', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(form),
})
if (resp.ok) {
setShowWizard(false)
setForm({ ...INITIAL_FORM })
setWizardStep(1)
loadRegistrations()
} else {
const data = await resp.json()
setError(data.error || 'Fehler beim Erstellen')
}
} catch {
setError('Netzwerkfehler')
} finally {
setSubmitting(false)
}
}
async function handleExport(id: string) {
try {
const resp = await fetch(`/api/sdk/v1/ai-registration/${id}`)
if (resp.ok) {
const reg = await resp.json()
// Build export JSON client-side
const exportData = {
schema_version: '1.0',
submission_type: 'ai_system_registration',
regulation: 'EU AI Act (EU) 2024/1689',
article: 'Art. 49',
provider: { name: reg.provider_name, address: reg.provider_address, country: reg.provider_country },
system: { name: reg.system_name, version: reg.system_version, description: reg.system_description, purpose: reg.intended_purpose },
classification: { risk_level: reg.risk_classification, annex_iii: reg.annex_iii_category, gpai: reg.gpai_classification },
conformity: { type: reg.conformity_assessment_type, ce_marking: reg.ce_marking },
}
const blob = new Blob([JSON.stringify(exportData, null, 2)], { type: 'application/json' })
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = `eu_ai_registration_${reg.system_name.replace(/\s+/g, '_')}.json`
a.click()
URL.revokeObjectURL(url)
}
} catch {
setError('Export fehlgeschlagen')
}
}
async function handleStatusChange(id: string, status: string) {
try {
await fetch(`/api/sdk/v1/ai-registration/${id}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ status }),
})
loadRegistrations()
} catch {
setError('Status-Aenderung fehlgeschlagen')
}
}
const updateForm = (updates: Partial<typeof form>) => setForm(prev => ({ ...prev, ...updates }))
const STEPS = [
{ id: 1, title: 'Anbieter', desc: 'Unternehmensangaben' },
{ id: 2, title: 'System', desc: 'KI-System Details' },
{ id: 3, title: 'Klassifikation', desc: 'Risikoeinstufung' },
{ id: 4, title: 'Konformitaet', desc: 'CE & Notified Body' },
{ id: 5, title: 'Trainingsdaten', desc: 'Datenzusammenfassung' },
{ id: 6, title: 'Pruefung', desc: 'Zusammenfassung & Export' },
]
return (
<div className="max-w-5xl mx-auto p-6">
{/* Header */}
<div className="flex items-center justify-between mb-8">
<div>
<h1 className="text-2xl font-bold text-gray-900">EU AI Database Registrierung</h1>
<p className="text-sm text-gray-500 mt-1">Art. 49 KI-Verordnung (EU) 2024/1689 Registrierung von Hochrisiko-KI-Systemen</p>
</div>
<button
onClick={() => setShowWizard(true)}
className="px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors"
>
+ Neue Registrierung
</button>
</div>
{error && (
<div className="mb-4 p-3 bg-red-50 border border-red-200 rounded-lg text-sm text-red-700">
{error}
<button onClick={() => setError(null)} className="ml-2 underline">Schliessen</button>
</div>
)}
{/* Stats */}
<div className="grid grid-cols-4 gap-4 mb-8">
{['draft', 'ready', 'submitted', 'registered'].map(status => {
const count = registrations.filter(r => r.registration_status === status).length
const style = STATUS_STYLES[status]
return (
<div key={status} className={`p-4 rounded-xl border ${style.bg}`}>
<div className={`text-2xl font-bold ${style.text}`}>{count}</div>
<div className="text-sm text-gray-600">{style.label}</div>
</div>
)
})}
</div>
{/* Registrations List */}
{loading ? (
<div className="text-center py-12 text-gray-500">Lade...</div>
) : registrations.length === 0 ? (
<div className="text-center py-12 text-gray-400">
<p className="text-lg mb-2">Noch keine Registrierungen</p>
<p className="text-sm">Erstelle eine neue Registrierung fuer dein Hochrisiko-KI-System.</p>
</div>
) : (
<div className="space-y-4">
{registrations.map(reg => {
const status = STATUS_STYLES[reg.registration_status] || STATUS_STYLES.draft
const risk = RISK_STYLES[reg.risk_classification] || RISK_STYLES.not_classified
return (
<div key={reg.id} className="bg-white rounded-xl border border-gray-200 p-6 hover:border-purple-300 transition-all">
<div className="flex items-center justify-between">
<div>
<div className="flex items-center gap-2 mb-1">
<h3 className="text-lg font-semibold text-gray-900">{reg.system_name}</h3>
<span className="text-sm text-gray-400">v{reg.system_version}</span>
<span className={`px-2 py-0.5 text-xs rounded-full ${status.bg} ${status.text}`}>{status.label}</span>
<span className={`px-2 py-0.5 text-xs rounded-full ${risk.bg} ${risk.text}`}>{reg.risk_classification.replace('_', ' ')}</span>
{reg.gpai_classification !== 'none' && (
<span className="px-2 py-0.5 text-xs rounded-full bg-blue-100 text-blue-700">GPAI: {reg.gpai_classification}</span>
)}
</div>
<div className="text-sm text-gray-500">
{reg.provider_name && <span>{reg.provider_name} · </span>}
{reg.eu_database_id && <span>EU-ID: {reg.eu_database_id} · </span>}
<span>{new Date(reg.created_at).toLocaleDateString('de-DE')}</span>
</div>
</div>
<div className="flex gap-2">
<button onClick={() => handleExport(reg.id)} className="px-3 py-1.5 text-sm border border-gray-300 rounded-lg hover:bg-gray-50">
JSON Export
</button>
{reg.registration_status === 'draft' && (
<button onClick={() => handleStatusChange(reg.id, 'ready')} className="px-3 py-1.5 text-sm bg-blue-600 text-white rounded-lg hover:bg-blue-700">
Bereit markieren
</button>
)}
{reg.registration_status === 'ready' && (
<button onClick={() => handleStatusChange(reg.id, 'submitted')} className="px-3 py-1.5 text-sm bg-green-600 text-white rounded-lg hover:bg-green-700">
Als eingereicht markieren
</button>
)}
</div>
</div>
</div>
)
})}
</div>
)}
{/* Wizard Modal */}
{showWizard && (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
<div className="bg-white rounded-2xl shadow-2xl w-full max-w-3xl max-h-[90vh] overflow-y-auto">
<div className="p-6 border-b">
<div className="flex items-center justify-between mb-4">
<h2 className="text-xl font-bold text-gray-900">Neue EU AI Registrierung</h2>
<button onClick={() => { setShowWizard(false); setWizardStep(1) }} className="text-gray-400 hover:text-gray-600 text-2xl">&times;</button>
</div>
{/* Step Indicator */}
<div className="flex gap-1">
{STEPS.map(step => (
<button key={step.id} onClick={() => setWizardStep(step.id)}
className={`flex-1 py-2 text-xs rounded-lg transition-all ${
wizardStep === step.id ? 'bg-purple-100 text-purple-700 font-medium' :
wizardStep > step.id ? 'bg-green-50 text-green-700' : 'bg-gray-50 text-gray-400'
}`}>
{wizardStep > step.id ? '✓ ' : ''}{step.title}
</button>
))}
</div>
</div>
<div className="p-6 space-y-4">
{/* Step 1: Provider */}
{wizardStep === 1 && (
<>
<h3 className="font-semibold text-gray-900">Anbieter-Informationen</h3>
<p className="text-sm text-gray-500">Angaben zum Anbieter des KI-Systems gemaess Art. 49 KI-VO.</p>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Firmenname *</label>
<input value={form.provider_name} onChange={e => updateForm({ provider_name: e.target.value })}
className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent" placeholder="Acme GmbH" />
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Rechtsform</label>
<input value={form.provider_legal_form} onChange={e => updateForm({ provider_legal_form: e.target.value })}
className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent" placeholder="GmbH" />
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Adresse</label>
<input value={form.provider_address} onChange={e => updateForm({ provider_address: e.target.value })}
className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent" placeholder="Musterstr. 1, 20095 Hamburg" />
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Land</label>
<select value={form.provider_country} onChange={e => updateForm({ provider_country: e.target.value })}
className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent">
<option value="DE">Deutschland</option>
<option value="AT">Oesterreich</option>
<option value="CH">Schweiz</option>
<option value="OTHER">Anderes Land</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">EU-Repraesentant (falls Non-EU)</label>
<input value={form.eu_representative_name} onChange={e => updateForm({ eu_representative_name: e.target.value })}
className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent" placeholder="Optional" />
</div>
</div>
</>
)}
{/* Step 2: System */}
{wizardStep === 2 && (
<>
<h3 className="font-semibold text-gray-900">KI-System Details</h3>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Systemname *</label>
<input value={form.system_name} onChange={e => updateForm({ system_name: e.target.value })}
className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent" placeholder="z.B. HR Copilot" />
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Version</label>
<input value={form.system_version} onChange={e => updateForm({ system_version: e.target.value })}
className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent" placeholder="1.0" />
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Systembeschreibung</label>
<textarea value={form.system_description} onChange={e => updateForm({ system_description: e.target.value })} rows={3}
className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent" placeholder="Beschreibe was das KI-System tut..." />
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Einsatzzweck (Intended Purpose)</label>
<textarea value={form.intended_purpose} onChange={e => updateForm({ intended_purpose: e.target.value })} rows={2}
className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent" placeholder="Wofuer wird das System eingesetzt?" />
</div>
</>
)}
{/* Step 3: Classification */}
{wizardStep === 3 && (
<>
<h3 className="font-semibold text-gray-900">Risiko-Klassifikation</h3>
<p className="text-sm text-gray-500">Basierend auf dem AI Act Decision Tree oder manueller Einstufung.</p>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Risikoklasse</label>
<select value={form.risk_classification} onChange={e => updateForm({ risk_classification: e.target.value })}
className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent">
<option value="not_classified">Noch nicht klassifiziert</option>
<option value="minimal_risk">Minimal Risk</option>
<option value="limited_risk">Limited Risk</option>
<option value="high_risk">High Risk</option>
</select>
</div>
{form.risk_classification === 'high_risk' && (
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Annex III Kategorie</label>
<select value={form.annex_iii_category} onChange={e => updateForm({ annex_iii_category: e.target.value })}
className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent">
<option value="">Bitte waehlen...</option>
<option value="biometric">1. Biometrische Identifizierung</option>
<option value="critical_infrastructure">2. Kritische Infrastruktur</option>
<option value="education">3. Bildung und Berufsausbildung</option>
<option value="employment">4. Beschaeftigung und Arbeitnehmerverwaltung</option>
<option value="essential_services">5. Zugang zu wesentlichen Diensten</option>
<option value="law_enforcement">6. Strafverfolgung</option>
<option value="migration">7. Migration und Grenzkontrolle</option>
<option value="justice">8. Rechtspflege und Demokratie</option>
</select>
</div>
)}
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">GPAI Klassifikation</label>
<select value={form.gpai_classification} onChange={e => updateForm({ gpai_classification: e.target.value })}
className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent">
<option value="none">Kein GPAI</option>
<option value="standard">GPAI (Standard)</option>
<option value="systemic">GPAI mit systemischem Risiko</option>
</select>
</div>
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4 text-sm text-blue-800">
<strong>Tipp:</strong> Nutze den <a href="/sdk/ai-act" className="underline">AI Act Decision Tree</a> fuer eine strukturierte Klassifikation.
</div>
</>
)}
{/* Step 4: Conformity */}
{wizardStep === 4 && (
<>
<h3 className="font-semibold text-gray-900">Konformitaetsbewertung</h3>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Art der Konformitaetsbewertung</label>
<select value={form.conformity_assessment_type} onChange={e => updateForm({ conformity_assessment_type: e.target.value })}
className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent">
<option value="not_required">Nicht erforderlich</option>
<option value="internal">Interne Konformitaetsbewertung</option>
<option value="third_party">Drittpartei-Bewertung (Notified Body)</option>
</select>
</div>
{form.conformity_assessment_type === 'third_party' && (
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Notified Body Name</label>
<input value={form.notified_body_name} onChange={e => updateForm({ notified_body_name: e.target.value })}
className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent" />
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Notified Body ID</label>
<input value={form.notified_body_id} onChange={e => updateForm({ notified_body_id: e.target.value })}
className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent" />
</div>
</div>
)}
<label className="flex items-center gap-3 p-3 rounded-lg border hover:bg-gray-50 cursor-pointer">
<input type="checkbox" checked={form.ce_marking} onChange={e => updateForm({ ce_marking: e.target.checked })}
className="w-4 h-4 rounded border-gray-300 text-purple-600" />
<span className="text-sm font-medium text-gray-900">CE-Kennzeichnung angebracht</span>
</label>
</>
)}
{/* Step 5: Training Data */}
{wizardStep === 5 && (
<>
<h3 className="font-semibold text-gray-900">Trainingsdaten-Zusammenfassung</h3>
<p className="text-sm text-gray-500">Art. 10 KI-VO Keine vollstaendige Offenlegung, sondern Kategorien und Herkunft.</p>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Zusammenfassung der Trainingsdaten</label>
<textarea value={form.training_data_summary} onChange={e => updateForm({ training_data_summary: e.target.value })} rows={5}
className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent"
placeholder="Beschreibe die verwendeten Datenquellen:&#10;- Oeffentliche Daten (z.B. Wikipedia, Common Crawl)&#10;- Lizenzierte Daten (z.B. Fachpublikationen)&#10;- Synthetische Daten&#10;- Unternehmensinterne Daten" />
</div>
</>
)}
{/* Step 6: Review */}
{wizardStep === 6 && (
<>
<h3 className="font-semibold text-gray-900">Zusammenfassung</h3>
<div className="space-y-3 text-sm">
<div className="grid grid-cols-2 gap-4 p-4 bg-gray-50 rounded-lg">
<div><span className="text-gray-500">Anbieter:</span> <strong>{form.provider_name || ''}</strong></div>
<div><span className="text-gray-500">Land:</span> <strong>{form.provider_country}</strong></div>
<div><span className="text-gray-500">System:</span> <strong>{form.system_name || ''}</strong></div>
<div><span className="text-gray-500">Version:</span> <strong>{form.system_version}</strong></div>
<div><span className="text-gray-500">Risiko:</span> <strong>{form.risk_classification}</strong></div>
<div><span className="text-gray-500">GPAI:</span> <strong>{form.gpai_classification}</strong></div>
<div><span className="text-gray-500">Konformitaet:</span> <strong>{form.conformity_assessment_type}</strong></div>
<div><span className="text-gray-500">CE:</span> <strong>{form.ce_marking ? 'Ja' : 'Nein'}</strong></div>
</div>
{form.intended_purpose && (
<div className="p-4 bg-gray-50 rounded-lg">
<span className="text-gray-500">Zweck:</span> {form.intended_purpose}
</div>
)}
</div>
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-4 text-sm text-yellow-800">
<strong>Hinweis:</strong> Die EU AI Datenbank befindet sich noch im Aufbau. Die Registrierung wird lokal gespeichert und kann spaeter uebermittelt werden.
</div>
</>
)}
</div>
{/* Navigation */}
<div className="p-6 border-t flex justify-between">
<button onClick={() => wizardStep > 1 ? setWizardStep(wizardStep - 1) : setShowWizard(false)}
className="px-4 py-2 text-gray-700 border rounded-lg hover:bg-gray-50">
{wizardStep === 1 ? 'Abbrechen' : 'Zurueck'}
</button>
{wizardStep < 6 ? (
<button onClick={() => setWizardStep(wizardStep + 1)}
disabled={wizardStep === 2 && !form.system_name}
className="px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 disabled:opacity-50">
Weiter
</button>
) : (
<button onClick={handleSubmit} disabled={submitting || !form.system_name}
className="px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 disabled:opacity-50">
{submitting ? 'Speichere...' : 'Registrierung erstellen'}
</button>
)}
</div>
</div>
</div>
)}
</div>
)
}

View File

@@ -0,0 +1,496 @@
'use client'
import React, { useState, useEffect } from 'react'
interface PaymentControl {
control_id: string
domain: string
title: string
objective: string
check_target: string
evidence: string[]
automation: string
}
interface PaymentDomain {
id: string
name: string
description: string
}
interface Assessment {
id: string
project_name: string
tender_reference: string
customer_name: string
system_type: string
total_controls: number
controls_passed: number
controls_failed: number
controls_partial: number
controls_not_applicable: number
controls_not_checked: number
compliance_score: number
status: string
created_at: string
}
interface TenderAnalysis {
id: string
file_name: string
file_size: number
project_name: string
customer_name: string
status: string
total_requirements: number
matched_count: number
unmatched_count: number
partial_count: number
requirements?: Array<{ req_id: string; text: string; obligation_level: string; technical_domain: string; confidence: number }>
match_results?: Array<{ req_id: string; req_text: string; verdict: string; matched_controls: Array<{ control_id: string; title: string; relevance: number }>; gap_description?: string }>
created_at: string
}
const AUTOMATION_STYLES: Record<string, { bg: string; text: string }> = {
high: { bg: 'bg-green-100', text: 'text-green-700' },
medium: { bg: 'bg-yellow-100', text: 'text-yellow-700' },
partial: { bg: 'bg-orange-100', text: 'text-orange-700' },
low: { bg: 'bg-red-100', text: 'text-red-700' },
}
const TARGET_ICONS: Record<string, string> = {
code: '💻', system: '🖥️', config: '⚙️', process: '📋',
repository: '📦', certificate: '📜',
}
export default function PaymentCompliancePage() {
const [controls, setControls] = useState<PaymentControl[]>([])
const [domains, setDomains] = useState<PaymentDomain[]>([])
const [assessments, setAssessments] = useState<Assessment[]>([])
const [tenderAnalyses, setTenderAnalyses] = useState<TenderAnalysis[]>([])
const [selectedTender, setSelectedTender] = useState<TenderAnalysis | null>(null)
const [selectedDomain, setSelectedDomain] = useState<string>('all')
const [loading, setLoading] = useState(true)
const [tab, setTab] = useState<'controls' | 'assessments' | 'tender'>('controls')
const [uploading, setUploading] = useState(false)
const [processing, setProcessing] = useState(false)
const [showNewAssessment, setShowNewAssessment] = useState(false)
const [newProject, setNewProject] = useState({ project_name: '', tender_reference: '', customer_name: '', system_type: 'full_stack' })
useEffect(() => {
loadData()
}, [])
async function loadData() {
try {
setLoading(true)
const [ctrlResp, assessResp, tenderResp] = await Promise.all([
fetch('/api/sdk/v1/payment-compliance?endpoint=controls'),
fetch('/api/sdk/v1/payment-compliance?endpoint=assessments'),
fetch('/api/sdk/v1/payment-compliance/tender'),
])
if (ctrlResp.ok) {
const data = await ctrlResp.json()
setControls(data.controls || [])
setDomains(data.domains || [])
}
if (assessResp.ok) {
const data = await assessResp.json()
setAssessments(data.assessments || [])
}
if (tenderResp.ok) {
const data = await tenderResp.json()
setTenderAnalyses(data.analyses || [])
}
} catch {}
finally { setLoading(false) }
}
async function handleTenderUpload(e: React.ChangeEvent<HTMLInputElement>) {
const file = e.target.files?.[0]
if (!file) return
setUploading(true)
try {
const formData = new FormData()
formData.append('file', file)
formData.append('project_name', file.name.replace(/\.[^.]+$/, ''))
const resp = await fetch('/api/sdk/v1/payment-compliance/tender', { method: 'POST', body: formData })
if (resp.ok) {
const data = await resp.json()
// Auto-start extraction + matching
setProcessing(true)
const extractResp = await fetch(`/api/sdk/v1/payment-compliance/tender/${data.id}?action=extract`, { method: 'POST' })
if (extractResp.ok) {
await fetch(`/api/sdk/v1/payment-compliance/tender/${data.id}?action=match`, { method: 'POST' })
}
// Reload and show result
const detailResp = await fetch(`/api/sdk/v1/payment-compliance/tender/${data.id}`)
if (detailResp.ok) {
const detail = await detailResp.json()
setSelectedTender(detail)
}
loadData()
}
} catch {} finally {
setUploading(false)
setProcessing(false)
}
}
async function handleViewTender(id: string) {
const resp = await fetch(`/api/sdk/v1/payment-compliance/tender/${id}`)
if (resp.ok) {
setSelectedTender(await resp.json())
}
}
async function handleCreateAssessment() {
const resp = await fetch('/api/sdk/v1/payment-compliance', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(newProject),
})
if (resp.ok) {
setShowNewAssessment(false)
setNewProject({ project_name: '', tender_reference: '', customer_name: '', system_type: 'full_stack' })
loadData()
}
}
const filteredControls = selectedDomain === 'all'
? controls
: controls.filter(c => c.domain === selectedDomain)
const domainStats = domains.map(d => ({
...d,
count: controls.filter(c => c.domain === d.id).length,
}))
return (
<div className="max-w-6xl mx-auto p-6">
{/* Header */}
<div className="flex items-center justify-between mb-8">
<div>
<h1 className="text-2xl font-bold text-gray-900">Payment Terminal Compliance</h1>
<p className="text-sm text-gray-500 mt-1">
Technische Pruefbibliothek fuer Zahlungssysteme {controls.length} Controls in {domains.length} Domaenen
</p>
</div>
<div className="flex gap-2">
<button onClick={() => setTab('controls')}
className={`px-4 py-2 rounded-lg text-sm font-medium ${tab === 'controls' ? 'bg-purple-600 text-white' : 'bg-gray-100 text-gray-700'}`}>
Controls ({controls.length})
</button>
<button onClick={() => setTab('assessments')}
className={`px-4 py-2 rounded-lg text-sm font-medium ${tab === 'assessments' ? 'bg-purple-600 text-white' : 'bg-gray-100 text-gray-700'}`}>
Assessments ({assessments.length})
</button>
<button onClick={() => setTab('tender')}
className={`px-4 py-2 rounded-lg text-sm font-medium ${tab === 'tender' ? 'bg-purple-600 text-white' : 'bg-gray-100 text-gray-700'}`}>
Ausschreibung ({tenderAnalyses.length})
</button>
</div>
</div>
{/* Info Box */}
<div className="mb-6 p-4 bg-blue-50 border border-blue-200 rounded-xl text-sm text-blue-800">
<div className="font-semibold mb-2">Wie funktioniert Payment Terminal Compliance?</div>
<div className="grid grid-cols-3 gap-4">
<div>
<div className="font-medium mb-1">1. Controls durchsuchen</div>
<p className="text-xs text-blue-700">Unsere Bibliothek enthaelt {controls.length} technische Pruefregeln fuer Zahlungssysteme von Transaktionslogik ueber Kryptographie bis ZVT/OPI-Protokollverhalten. Jeder Control definiert was geprueft wird und welche Evidenz noetig ist.</p>
</div>
<div>
<div className="font-medium mb-1">2. Assessment erstellen</div>
<p className="text-xs text-blue-700">Ein Assessment ist eine projektbezogene Pruefung z.B. fuer eine bestimmte Ausschreibung oder einen Kunden. Sie ordnet jedem Control einen Status zu: bestanden, fehlgeschlagen, teilweise oder nicht anwendbar.</p>
</div>
<div>
<div className="font-medium mb-1">3. Ausschreibung analysieren</div>
<p className="text-xs text-blue-700">Laden Sie ein Ausschreibungsdokument hoch. Die KI extrahiert automatisch die Anforderungen und matcht sie gegen unsere Controls. Ergebnis: Welche Anforderungen sind abgedeckt und wo gibt es Luecken.</p>
</div>
</div>
</div>
{loading ? (
<div className="text-center py-12 text-gray-500">Lade...</div>
) : tab === 'controls' ? (
<>
{/* Domain Filter */}
<div className="grid grid-cols-5 gap-3 mb-6">
<button onClick={() => setSelectedDomain('all')}
className={`p-3 rounded-xl border text-center ${selectedDomain === 'all' ? 'border-purple-500 bg-purple-50' : 'border-gray-200 hover:border-purple-300'}`}>
<div className="text-lg font-bold text-purple-700">{controls.length}</div>
<div className="text-xs text-gray-500">Alle</div>
</button>
{domainStats.map(d => (
<button key={d.id} onClick={() => setSelectedDomain(d.id)}
className={`p-3 rounded-xl border text-center ${selectedDomain === d.id ? 'border-purple-500 bg-purple-50' : 'border-gray-200 hover:border-purple-300'}`}>
<div className="text-lg font-bold text-gray-900">{d.count}</div>
<div className="text-xs text-gray-500 truncate">{d.id}</div>
</button>
))}
</div>
{/* Domain Description */}
{selectedDomain !== 'all' && (
<div className="mb-4 p-3 bg-blue-50 border border-blue-200 rounded-lg text-sm text-blue-800">
<strong>{domains.find(d => d.id === selectedDomain)?.name}:</strong>{' '}
{domains.find(d => d.id === selectedDomain)?.description}
</div>
)}
{/* Controls List */}
<div className="space-y-3">
{filteredControls.map(ctrl => {
const autoStyle = AUTOMATION_STYLES[ctrl.automation] || AUTOMATION_STYLES.low
return (
<div key={ctrl.control_id} className="bg-white rounded-xl border border-gray-200 p-4 hover:border-purple-300 transition-all">
<div className="flex items-start justify-between">
<div className="flex-1">
<div className="flex items-center gap-2 mb-1">
<span className="text-xs font-mono text-purple-600 bg-purple-50 px-2 py-0.5 rounded">{ctrl.control_id}</span>
<span className="text-xs text-gray-400">{TARGET_ICONS[ctrl.check_target] || '🔍'} {ctrl.check_target}</span>
<span className={`text-xs px-2 py-0.5 rounded-full ${autoStyle.bg} ${autoStyle.text}`}>
{ctrl.automation}
</span>
</div>
<h3 className="text-sm font-semibold text-gray-900">{ctrl.title}</h3>
<p className="text-xs text-gray-500 mt-1">{ctrl.objective}</p>
</div>
</div>
<div className="flex gap-1 mt-2">
{ctrl.evidence.map(ev => (
<span key={ev} className="text-xs bg-gray-100 text-gray-600 px-2 py-0.5 rounded">{ev}</span>
))}
</div>
</div>
)
})}
</div>
</>
) : tab === 'assessments' ? (
<>
{/* Assessments Tab */}
<div className="mb-4">
<button onClick={() => setShowNewAssessment(true)}
className="px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700">
+ Neues Assessment
</button>
</div>
{showNewAssessment && (
<div className="mb-6 p-6 bg-white rounded-xl border border-purple-200">
<h3 className="font-semibold text-gray-900 mb-4">Neues Payment Compliance Assessment</h3>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Projektname *</label>
<input value={newProject.project_name} onChange={e => setNewProject(p => ({ ...p, project_name: e.target.value }))}
className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-purple-500" placeholder="z.B. Ausschreibung Muenchen 2026" />
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Ausschreibungs-Referenz</label>
<input value={newProject.tender_reference} onChange={e => setNewProject(p => ({ ...p, tender_reference: e.target.value }))}
className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-purple-500" placeholder="z.B. 2026-PAY-001" />
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Kunde</label>
<input value={newProject.customer_name} onChange={e => setNewProject(p => ({ ...p, customer_name: e.target.value }))}
className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-purple-500" placeholder="z.B. Stadt Muenchen" />
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Systemtyp</label>
<select value={newProject.system_type} onChange={e => setNewProject(p => ({ ...p, system_type: e.target.value }))}
className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-purple-500">
<option value="full_stack">Full Stack (Terminal + Backend)</option>
<option value="terminal">Nur Terminal</option>
<option value="backend">Nur Backend</option>
</select>
</div>
</div>
<div className="flex gap-2 mt-4">
<button onClick={handleCreateAssessment} disabled={!newProject.project_name}
className="px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 disabled:opacity-50">Erstellen</button>
<button onClick={() => setShowNewAssessment(false)}
className="px-4 py-2 bg-gray-100 text-gray-700 rounded-lg hover:bg-gray-200">Abbrechen</button>
</div>
</div>
)}
{assessments.length === 0 ? (
<div className="text-center py-12 text-gray-400">
<p className="text-lg mb-2">Noch keine Assessments</p>
<p className="text-sm">Erstelle ein neues Assessment fuer eine Ausschreibung.</p>
</div>
) : (
<div className="space-y-4">
{assessments.map(a => (
<div key={a.id} className="bg-white rounded-xl border border-gray-200 p-6">
<div className="flex items-center justify-between mb-3">
<div>
<h3 className="text-lg font-semibold text-gray-900">{a.project_name}</h3>
<div className="text-sm text-gray-500">
{a.customer_name && <span>{a.customer_name} · </span>}
{a.tender_reference && <span>Ref: {a.tender_reference} · </span>}
<span>{new Date(a.created_at).toLocaleDateString('de-DE')}</span>
</div>
</div>
<span className={`px-3 py-1 rounded-full text-sm font-medium ${
a.status === 'completed' ? 'bg-green-100 text-green-700' :
a.status === 'in_progress' ? 'bg-yellow-100 text-yellow-700' :
'bg-gray-100 text-gray-700'
}`}>{a.status}</span>
</div>
<div className="grid grid-cols-6 gap-2">
<div className="text-center p-2 bg-gray-50 rounded">
<div className="text-lg font-bold">{a.total_controls}</div>
<div className="text-xs text-gray-500">Total</div>
</div>
<div className="text-center p-2 bg-green-50 rounded">
<div className="text-lg font-bold text-green-700">{a.controls_passed}</div>
<div className="text-xs text-gray-500">Passed</div>
</div>
<div className="text-center p-2 bg-red-50 rounded">
<div className="text-lg font-bold text-red-700">{a.controls_failed}</div>
<div className="text-xs text-gray-500">Failed</div>
</div>
<div className="text-center p-2 bg-yellow-50 rounded">
<div className="text-lg font-bold text-yellow-700">{a.controls_partial}</div>
<div className="text-xs text-gray-500">Partial</div>
</div>
<div className="text-center p-2 bg-gray-50 rounded">
<div className="text-lg font-bold text-gray-400">{a.controls_not_applicable}</div>
<div className="text-xs text-gray-500">N/A</div>
</div>
<div className="text-center p-2 bg-gray-50 rounded">
<div className="text-lg font-bold text-gray-400">{a.controls_not_checked}</div>
<div className="text-xs text-gray-500">Offen</div>
</div>
</div>
</div>
))}
</div>
)}
</>
) : tab === 'tender' ? (
<>
{/* Tender Analysis Tab */}
<div className="mb-6 p-6 bg-white rounded-xl border-2 border-dashed border-purple-300 text-center">
<h3 className="text-lg font-semibold text-gray-900 mb-2">Ausschreibung analysieren</h3>
<p className="text-sm text-gray-500 mb-4">
Laden Sie ein Ausschreibungsdokument hoch. Die KI extrahiert automatisch alle Anforderungen und matcht sie gegen die Control-Bibliothek.
</p>
<label className="inline-block px-6 py-3 bg-purple-600 text-white rounded-lg hover:bg-purple-700 cursor-pointer">
{uploading ? 'Hochladen...' : processing ? 'Analysiere...' : 'PDF / Dokument hochladen'}
<input type="file" className="hidden" accept=".pdf,.txt,.doc,.docx" onChange={handleTenderUpload} disabled={uploading || processing} />
</label>
<p className="text-xs text-gray-400 mt-2">PDF, TXT oder Word. Max 50 MB. Dokument wird nur fuer diese Analyse verwendet.</p>
</div>
{/* Selected Tender Detail */}
{selectedTender && (
<div className="mb-6 p-6 bg-white rounded-xl border border-purple-200">
<div className="flex items-center justify-between mb-4">
<div>
<h3 className="text-lg font-semibold text-gray-900">{selectedTender.project_name}</h3>
<p className="text-sm text-gray-500">{selectedTender.file_name} {selectedTender.status}</p>
</div>
<button onClick={() => setSelectedTender(null)} className="text-gray-400 hover:text-gray-600 text-xl">&times;</button>
</div>
{/* Stats */}
<div className="grid grid-cols-4 gap-3 mb-6">
<div className="text-center p-3 bg-gray-50 rounded-lg">
<div className="text-2xl font-bold">{selectedTender.total_requirements}</div>
<div className="text-xs text-gray-500">Anforderungen</div>
</div>
<div className="text-center p-3 bg-green-50 rounded-lg">
<div className="text-2xl font-bold text-green-700">{selectedTender.matched_count}</div>
<div className="text-xs text-gray-500">Abgedeckt</div>
</div>
<div className="text-center p-3 bg-yellow-50 rounded-lg">
<div className="text-2xl font-bold text-yellow-700">{selectedTender.partial_count}</div>
<div className="text-xs text-gray-500">Teilweise</div>
</div>
<div className="text-center p-3 bg-red-50 rounded-lg">
<div className="text-2xl font-bold text-red-700">{selectedTender.unmatched_count}</div>
<div className="text-xs text-gray-500">Luecken</div>
</div>
</div>
{/* Match Results */}
{selectedTender.match_results && selectedTender.match_results.length > 0 && (
<div className="space-y-3">
<h4 className="font-semibold text-gray-900">Requirement Control Matching</h4>
{selectedTender.match_results.map((mr, idx) => (
<div key={idx} className={`p-4 rounded-lg border ${
mr.verdict === 'matched' ? 'border-green-200 bg-green-50' :
mr.verdict === 'partial' ? 'border-yellow-200 bg-yellow-50' :
'border-red-200 bg-red-50'
}`}>
<div className="flex items-start justify-between mb-2">
<div className="flex-1">
<div className="flex items-center gap-2 mb-1">
<span className="text-xs font-mono bg-white px-2 py-0.5 rounded border">{mr.req_id}</span>
<span className={`text-xs px-2 py-0.5 rounded-full ${
mr.verdict === 'matched' ? 'bg-green-200 text-green-800' :
mr.verdict === 'partial' ? 'bg-yellow-200 text-yellow-800' :
'bg-red-200 text-red-800'
}`}>
{mr.verdict === 'matched' ? 'Abgedeckt' : mr.verdict === 'partial' ? 'Teilweise' : 'Luecke'}
</span>
</div>
<p className="text-sm text-gray-900">{mr.req_text}</p>
</div>
</div>
{mr.matched_controls && mr.matched_controls.length > 0 && (
<div className="mt-2 flex flex-wrap gap-1">
{mr.matched_controls.map(mc => (
<span key={mc.control_id} className="text-xs bg-white border px-2 py-0.5 rounded">
{mc.control_id} ({Math.round(mc.relevance * 100)}%)
</span>
))}
</div>
)}
{mr.gap_description && (
<p className="text-xs text-orange-700 mt-2">{mr.gap_description}</p>
)}
</div>
))}
</div>
)}
</div>
)}
{/* Previous Analyses */}
{tenderAnalyses.length > 0 && (
<div>
<h4 className="font-semibold text-gray-900 mb-3">Bisherige Analysen</h4>
<div className="space-y-3">
{tenderAnalyses.map(ta => (
<button key={ta.id} onClick={() => handleViewTender(ta.id)}
className="w-full text-left bg-white rounded-xl border border-gray-200 p-4 hover:border-purple-300 transition-all">
<div className="flex items-center justify-between">
<div>
<h3 className="font-medium text-gray-900">{ta.project_name}</h3>
<p className="text-xs text-gray-500">{ta.file_name} {new Date(ta.created_at).toLocaleDateString('de-DE')}</p>
</div>
<div className="flex gap-2">
<span className="text-xs bg-green-100 text-green-700 px-2 py-0.5 rounded-full">{ta.matched_count} matched</span>
{ta.unmatched_count > 0 && (
<span className="text-xs bg-red-100 text-red-700 px-2 py-0.5 rounded-full">{ta.unmatched_count} gaps</span>
)}
<span className={`text-xs px-2 py-0.5 rounded-full ${
ta.status === 'matched' ? 'bg-blue-100 text-blue-700' : 'bg-gray-100 text-gray-700'
}`}>{ta.status}</span>
</div>
</div>
</button>
))}
</div>
</div>
)}
</>
) : null}
</div>
)
}

View File

@@ -142,8 +142,8 @@ export const SDK_FLOW_STEPS: SDKFlowStep[] = [
checkpointId: 'CP-UC',
checkpointType: 'REQUIRED',
checkpointReviewer: 'NONE',
description: 'Systematische Erfassung aller Datenverarbeitungs- und KI-Anwendungsfaelle ueber einen 8-Schritte-Wizard mit Kachel-Auswahl.',
descriptionLong: 'In einem 8-Schritte-Wizard werden alle Use Cases erfasst: (1) Grundlegendes — Titel, Beschreibung, KI-Kategorie (21 Kacheln), Branche wird automatisch aus dem Profil abgeleitet. (2) Datenkategorien — ~60 Kategorien in 10 Gruppen als Kacheln (inkl. Art. 9 hervorgehoben). (3) Verarbeitungszweck — 16 Zweck-Kacheln, Rechtsgrundlage wird vom SDK automatisch ermittelt. (4) Automatisierungsgrad — assistiv/teilautomatisiert/vollautomatisiert. (5) Hosting & Modell — Provider, Region, Modellnutzung (Inferenz/RAG/Fine-Tuning/Training). (6) Datentransfer — Transferziele und Schutzmechanismen. (7) Datenhaltung — Aufbewahrungsfristen. (8) Vertraege — vorhandene Compliance-Dokumente. Die RAG-Collection bp_compliance_ce wird verwendet, um relevante CE-Regulierungen automatisch den Use Cases zuzuordnen (UCCA).',
description: 'Systematische Erfassung aller Datenverarbeitungs- und KI-Anwendungsfaelle ueber einen 8-Schritte-Wizard mit Kachel-Auswahl. Inkl. BetrVG-Mitbestimmungspruefung und Betriebsrats-Konflikt-Score.',
descriptionLong: 'In einem 8-Schritte-Wizard werden alle Use Cases erfasst: (1) Grundlegendes — Titel, Beschreibung, KI-Kategorie (21 Kacheln), Branche wird automatisch aus dem Profil abgeleitet. (2) Datenkategorien — ~60 Kategorien in 10 Gruppen als Kacheln (inkl. Art. 9 hervorgehoben). (3) Verarbeitungszweck — 16 Zweck-Kacheln, Rechtsgrundlage wird vom SDK automatisch ermittelt. (4) Automatisierungsgrad + BetrVG — assistiv/teilautomatisiert/vollautomatisiert, plus 3 BetrVG-Toggles: Ueberwachungseignung, HR-Entscheidungsunterstuetzung, BR-Konsultation. Das SDK berechnet daraus einen Betriebsrats-Konflikt-Score (0-100) und leitet BetrVG-Pflichten ab (§87, §90, §94, §95). (5) Hosting & Modell — Provider, Region, Modellnutzung (Inferenz/RAG/Fine-Tuning/Training). (6) Datentransfer — Transferziele und Schutzmechanismen. (7) Datenhaltung — Aufbewahrungsfristen. (8) Vertraege — vorhandene Compliance-Dokumente. Die RAG-Collection bp_compliance_ce wird verwendet, um relevante CE-Regulierungen automatisch den Use Cases zuzuordnen (UCCA). Die Collection bp_compliance_datenschutz enthaelt 14 BAG-Urteile zu IT-Mitbestimmung (M365, SAP, SaaS, Video).',
legalBasis: 'Art. 30 DSGVO (Verzeichnis von Verarbeitungstaetigkeiten)',
inputs: ['companyProfile'],
outputs: ['useCases'],
@@ -155,6 +155,27 @@ export const SDK_FLOW_STEPS: SDKFlowStep[] = [
isOptional: false,
url: '/sdk/use-cases',
},
{
id: 'ai-registration',
name: 'EU AI Database Registrierung',
nameShort: 'EU-Reg',
package: 'vorbereitung',
seq: 350,
checkpointId: 'CP-REG',
checkpointType: 'CONDITIONAL',
checkpointReviewer: 'NONE',
description: 'Registrierung von Hochrisiko-KI-Systemen in der EU AI Database gemaess Art. 49 KI-Verordnung.',
descriptionLong: 'Fuer Hochrisiko-KI-Systeme (Annex III) ist eine Registrierung in der EU AI Database Pflicht. Ein 6-Schritte-Wizard fuehrt durch den Prozess: (1) Anbieter-Daten — Name, Rechtsform, Adresse, EU-Repraesentant. (2) System-Details — Name, Version, Beschreibung, Einsatzzweck (vorausgefuellt aus UCCA Assessment). (3) Klassifikation — Risikoklasse und Annex III Kategorie (aus Decision Tree). (4) Konformitaet — CE-Kennzeichnung, Notified Body. (5) Trainingsdaten — Zusammenfassung der Datenquellen (Art. 10). (6) Pruefung + Export — JSON-Download fuer EU-Datenbank-Submission. Der Status-Workflow ist: Entwurf → Bereit → Eingereicht → Registriert.',
legalBasis: 'Art. 49 KI-Verordnung (EU) 2024/1689',
inputs: ['useCases', 'companyProfile'],
outputs: ['euRegistration'],
prerequisiteSteps: ['use-case-assessment'],
dbTables: ['ai_system_registrations'],
dbMode: 'read/write',
ragCollections: [],
isOptional: true,
url: '/sdk/ai-registration',
},
{
id: 'import',
name: 'Dokument-Import',

View File

@@ -57,6 +57,8 @@ interface FullAssessment {
dsfa_recommended: boolean
art22_risk: boolean
training_allowed: string
betrvg_conflict_score?: number
betrvg_consultation_required?: boolean
triggered_rules?: TriggeredRule[]
required_controls?: RequiredControl[]
recommended_architecture?: PatternRecommendation[]
@@ -167,6 +169,8 @@ export default function AssessmentDetailPage() {
dsfa_recommended: assessment.dsfa_recommended,
art22_risk: assessment.art22_risk,
training_allowed: assessment.training_allowed,
betrvg_conflict_score: assessment.betrvg_conflict_score,
betrvg_consultation_required: assessment.betrvg_consultation_required,
// AssessmentResultCard expects rule_code; backend stores code — map here
triggered_rules: assessment.triggered_rules?.map(r => ({
rule_code: r.code,

View File

@@ -10,6 +10,8 @@ interface Assessment {
feasibility: string
risk_level: string
risk_score: number
betrvg_conflict_score?: number
betrvg_consultation_required?: boolean
domain: string
created_at: string
}
@@ -194,6 +196,16 @@ export default function UseCasesPage() {
<span className={`px-2 py-0.5 text-xs rounded-full ${feasibility.bg} ${feasibility.text}`}>
{feasibility.label}
</span>
{assessment.betrvg_conflict_score != null && assessment.betrvg_conflict_score > 0 && (
<span className={`px-2 py-0.5 text-xs rounded-full ${
assessment.betrvg_conflict_score >= 75 ? 'bg-red-100 text-red-700' :
assessment.betrvg_conflict_score >= 50 ? 'bg-orange-100 text-orange-700' :
assessment.betrvg_conflict_score >= 25 ? 'bg-yellow-100 text-yellow-700' :
'bg-green-100 text-green-700'
}`}>
BR {assessment.betrvg_conflict_score}
</span>
)}
</div>
<div className="flex items-center gap-4 text-sm text-gray-500">
<span>{assessment.domain}</span>

View File

@@ -546,6 +546,89 @@ export function SDKSidebar({ collapsed = false, onCollapsedChange }: SDKSidebarP
/>
</div>
{/* KI-Compliance */}
<div className="border-t border-gray-100 py-2">
{!collapsed && (
<div className="px-4 py-2 text-xs font-medium text-gray-400 uppercase tracking-wider">
KI-Compliance
</div>
)}
<AdditionalModuleItem
href="/sdk/advisory-board"
icon={
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2" />
</svg>
}
label="Use Case Erfassung"
isActive={pathname === '/sdk/advisory-board'}
collapsed={collapsed}
projectId={projectId}
/>
<AdditionalModuleItem
href="/sdk/use-cases"
icon={
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
d="M4 6h16M4 10h16M4 14h16M4 18h16" />
</svg>
}
label="Use Cases"
isActive={pathname?.startsWith('/sdk/use-cases') ?? false}
collapsed={collapsed}
projectId={projectId}
/>
<AdditionalModuleItem
href="/sdk/ai-act"
icon={
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
d="M13 10V3L4 14h7v7l9-11h-7z" />
</svg>
}
label="AI Act"
isActive={pathname?.startsWith('/sdk/ai-act') ?? false}
collapsed={collapsed}
projectId={projectId}
/>
<AdditionalModuleItem
href="/sdk/ai-registration"
icon={
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4" />
</svg>
}
label="EU Registrierung"
isActive={pathname?.startsWith('/sdk/ai-registration') ?? false}
collapsed={collapsed}
projectId={projectId}
/>
</div>
{/* Payment Compliance */}
<div className="border-t border-gray-100 py-2">
{!collapsed && (
<div className="px-4 py-2 text-xs font-medium text-gray-400 uppercase tracking-wider">
Payment / Terminal
</div>
)}
<AdditionalModuleItem
href="/sdk/payment-compliance"
icon={
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
d="M3 10h18M7 15h1m4 0h1m-7 4h12a3 3 0 003-3V8a3 3 0 00-3-3H6a3 3 0 00-3 3v8a3 3 0 003 3z" />
</svg>
}
label="Payment Compliance"
isActive={pathname?.startsWith('/sdk/payment-compliance') ?? false}
collapsed={collapsed}
projectId={projectId}
/>
</div>
{/* Additional Modules */}
<div className="border-t border-gray-100 py-2">
{!collapsed && (

View File

@@ -0,0 +1,554 @@
'use client'
import React, { useState, useEffect, useCallback } from 'react'
// =============================================================================
// TYPES
// =============================================================================
interface DecisionTreeQuestion {
id: string
axis: 'high_risk' | 'gpai'
question: string
description: string
article_ref: string
skip_if?: string
}
interface DecisionTreeDefinition {
id: string
name: string
version: string
questions: DecisionTreeQuestion[]
}
interface DecisionTreeAnswer {
question_id: string
value: boolean
note?: string
}
interface GPAIClassification {
is_gpai: boolean
is_systemic_risk: boolean
gpai_category: 'none' | 'standard' | 'systemic'
applicable_articles: string[]
obligations: string[]
}
interface DecisionTreeResult {
id: string
tenant_id: string
system_name: string
system_description?: string
answers: Record<string, DecisionTreeAnswer>
high_risk_result: string
gpai_result: GPAIClassification
combined_obligations: string[]
applicable_articles: string[]
created_at: string
}
// =============================================================================
// CONSTANTS
// =============================================================================
const RISK_LEVEL_CONFIG: Record<string, { label: string; color: string; bg: string; border: string }> = {
unacceptable: { label: 'Unzulässig', color: 'text-red-700', bg: 'bg-red-50', border: 'border-red-200' },
high_risk: { label: 'Hochrisiko', color: 'text-orange-700', bg: 'bg-orange-50', border: 'border-orange-200' },
limited_risk: { label: 'Begrenztes Risiko', color: 'text-yellow-700', bg: 'bg-yellow-50', border: 'border-yellow-200' },
minimal_risk: { label: 'Minimales Risiko', color: 'text-green-700', bg: 'bg-green-50', border: 'border-green-200' },
not_applicable: { label: 'Nicht anwendbar', color: 'text-gray-500', bg: 'bg-gray-50', border: 'border-gray-200' },
}
const GPAI_CONFIG: Record<string, { label: string; color: string; bg: string; border: string }> = {
none: { label: 'Kein GPAI', color: 'text-gray-500', bg: 'bg-gray-50', border: 'border-gray-200' },
standard: { label: 'GPAI Standard', color: 'text-blue-700', bg: 'bg-blue-50', border: 'border-blue-200' },
systemic: { label: 'GPAI Systemisches Risiko', color: 'text-purple-700', bg: 'bg-purple-50', border: 'border-purple-200' },
}
// =============================================================================
// MAIN COMPONENT
// =============================================================================
export default function DecisionTreeWizard() {
const [definition, setDefinition] = useState<DecisionTreeDefinition | null>(null)
const [answers, setAnswers] = useState<Record<string, DecisionTreeAnswer>>({})
const [currentIdx, setCurrentIdx] = useState(0)
const [systemName, setSystemName] = useState('')
const [systemDescription, setSystemDescription] = useState('')
const [result, setResult] = useState<DecisionTreeResult | null>(null)
const [loading, setLoading] = useState(true)
const [saving, setSaving] = useState(false)
const [error, setError] = useState<string | null>(null)
const [phase, setPhase] = useState<'intro' | 'questions' | 'result'>('intro')
// Load decision tree definition
useEffect(() => {
const load = async () => {
try {
const res = await fetch('/api/sdk/v1/ucca/decision-tree')
if (res.ok) {
const data = await res.json()
setDefinition(data)
} else {
setError('Entscheidungsbaum konnte nicht geladen werden')
}
} catch {
setError('Verbindung zum Backend fehlgeschlagen')
} finally {
setLoading(false)
}
}
load()
}, [])
// Get visible questions (respecting skip logic)
const getVisibleQuestions = useCallback((): DecisionTreeQuestion[] => {
if (!definition) return []
return definition.questions.filter(q => {
if (!q.skip_if) return true
// Skip this question if the gate question was answered "no"
const gateAnswer = answers[q.skip_if]
if (gateAnswer && !gateAnswer.value) return false
return true
})
}, [definition, answers])
const visibleQuestions = getVisibleQuestions()
const currentQuestion = visibleQuestions[currentIdx]
const totalVisible = visibleQuestions.length
const highRiskQuestions = visibleQuestions.filter(q => q.axis === 'high_risk')
const gpaiQuestions = visibleQuestions.filter(q => q.axis === 'gpai')
const handleAnswer = (value: boolean) => {
if (!currentQuestion) return
setAnswers(prev => ({
...prev,
[currentQuestion.id]: {
question_id: currentQuestion.id,
value,
},
}))
// Auto-advance
if (currentIdx < totalVisible - 1) {
setCurrentIdx(prev => prev + 1)
}
}
const handleBack = () => {
if (currentIdx > 0) {
setCurrentIdx(prev => prev - 1)
}
}
const handleSubmit = async () => {
setSaving(true)
setError(null)
try {
const res = await fetch('/api/sdk/v1/ucca/decision-tree/evaluate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
system_name: systemName,
system_description: systemDescription,
answers,
}),
})
if (res.ok) {
const data = await res.json()
setResult(data)
setPhase('result')
} else {
const err = await res.json().catch(() => ({ error: 'Auswertung fehlgeschlagen' }))
setError(err.error || 'Auswertung fehlgeschlagen')
}
} catch {
setError('Verbindung zum Backend fehlgeschlagen')
} finally {
setSaving(false)
}
}
const handleReset = () => {
setAnswers({})
setCurrentIdx(0)
setSystemName('')
setSystemDescription('')
setResult(null)
setPhase('intro')
setError(null)
}
const allAnswered = visibleQuestions.every(q => answers[q.id] !== undefined)
if (loading) {
return (
<div className="bg-white rounded-xl border border-gray-200 p-12 text-center">
<div className="w-10 h-10 border-2 border-purple-500 border-t-transparent rounded-full animate-spin mx-auto mb-4" />
<p className="text-gray-500">Entscheidungsbaum wird geladen...</p>
</div>
)
}
if (error && !definition) {
return (
<div className="bg-red-50 border border-red-200 rounded-xl p-6 text-center">
<p className="text-red-700">{error}</p>
<p className="text-red-500 text-sm mt-2">Bitte stellen Sie sicher, dass der AI Compliance SDK Service läuft.</p>
</div>
)
}
// =========================================================================
// INTRO PHASE
// =========================================================================
if (phase === 'intro') {
return (
<div className="space-y-6">
<div className="bg-white rounded-xl border border-gray-200 p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-2">AI Act Entscheidungsbaum</h3>
<p className="text-sm text-gray-500 mb-6">
Klassifizieren Sie Ihr KI-System anhand von 12 Fragen auf zwei Achsen:
<strong> High-Risk</strong> (Anhang III) und <strong>GPAI</strong> (Art. 5156).
</p>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6">
<div className="p-4 bg-orange-50 border border-orange-200 rounded-lg">
<div className="flex items-center gap-2 mb-2">
<svg className="w-5 h-5 text-orange-600" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126z" />
</svg>
<span className="font-medium text-orange-700">Achse 1: High-Risk</span>
</div>
<p className="text-sm text-orange-600">7 Fragen zu Anhang III Kategorien (Biometrie, kritische Infrastruktur, Bildung, Beschäftigung, etc.)</p>
</div>
<div className="p-4 bg-blue-50 border border-blue-200 rounded-lg">
<div className="flex items-center gap-2 mb-2">
<svg className="w-5 h-5 text-blue-600" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M9.813 15.904L9 18.75l-.813-2.846a4.5 4.5 0 00-3.09-3.09L2.25 12l2.846-.813a4.5 4.5 0 003.09-3.09L9 5.25l.813 2.846a4.5 4.5 0 003.09 3.09L15.75 12l-2.846.813a4.5 4.5 0 00-3.09 3.09z" />
</svg>
<span className="font-medium text-blue-700">Achse 2: GPAI</span>
</div>
<p className="text-sm text-blue-600">5 Fragen zu General-Purpose AI (Foundation Models, systemisches Risiko, Art. 5156)</p>
</div>
</div>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Name des KI-Systems *</label>
<input
type="text"
value={systemName}
onChange={e => setSystemName(e.target.value)}
placeholder="z.B. Dokumenten-Analyse-KI, Chatbot-Service, Code-Assistent"
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Beschreibung (optional)</label>
<textarea
value={systemDescription}
onChange={e => setSystemDescription(e.target.value)}
placeholder="Kurze Beschreibung des KI-Systems und seines Einsatzzwecks..."
rows={2}
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent"
/>
</div>
</div>
<div className="mt-6 flex justify-end">
<button
onClick={() => setPhase('questions')}
disabled={!systemName.trim()}
className={`px-6 py-2 rounded-lg font-medium transition-colors ${
systemName.trim()
? 'bg-purple-600 text-white hover:bg-purple-700'
: 'bg-gray-200 text-gray-400 cursor-not-allowed'
}`}
>
Klassifizierung starten
</button>
</div>
</div>
</div>
)
}
// =========================================================================
// RESULT PHASE
// =========================================================================
if (phase === 'result' && result) {
const riskConfig = RISK_LEVEL_CONFIG[result.high_risk_result] || RISK_LEVEL_CONFIG.not_applicable
const gpaiConfig = GPAI_CONFIG[result.gpai_result.gpai_category] || GPAI_CONFIG.none
return (
<div className="space-y-6">
{/* Header */}
<div className="bg-white rounded-xl border border-gray-200 p-6">
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-semibold text-gray-900">Klassifizierungsergebnis: {result.system_name}</h3>
<button
onClick={handleReset}
className="px-4 py-2 text-sm text-purple-600 hover:bg-purple-50 rounded-lg transition-colors"
>
Neue Klassifizierung
</button>
</div>
{/* Two-Axis Result Cards */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6">
<div className={`p-5 rounded-xl border-2 ${riskConfig.border} ${riskConfig.bg}`}>
<div className="text-sm font-medium text-gray-500 mb-1">Achse 1: High-Risk (Anhang III)</div>
<div className={`text-xl font-bold ${riskConfig.color}`}>{riskConfig.label}</div>
</div>
<div className={`p-5 rounded-xl border-2 ${gpaiConfig.border} ${gpaiConfig.bg}`}>
<div className="text-sm font-medium text-gray-500 mb-1">Achse 2: GPAI (Art. 5156)</div>
<div className={`text-xl font-bold ${gpaiConfig.color}`}>{gpaiConfig.label}</div>
{result.gpai_result.is_systemic_risk && (
<div className="mt-1 text-xs text-purple-600 font-medium">Systemisches Risiko</div>
)}
</div>
</div>
</div>
{/* Applicable Articles */}
{result.applicable_articles && result.applicable_articles.length > 0 && (
<div className="bg-white rounded-xl border border-gray-200 p-6">
<h4 className="text-sm font-semibold text-gray-900 mb-3">Anwendbare Artikel</h4>
<div className="flex flex-wrap gap-2">
{result.applicable_articles.map(art => (
<span key={art} className="px-3 py-1 text-xs bg-indigo-50 text-indigo-700 rounded-full border border-indigo-200">
{art}
</span>
))}
</div>
</div>
)}
{/* Combined Obligations */}
{result.combined_obligations && result.combined_obligations.length > 0 && (
<div className="bg-white rounded-xl border border-gray-200 p-6">
<h4 className="text-sm font-semibold text-gray-900 mb-3">
Pflichten ({result.combined_obligations.length})
</h4>
<div className="space-y-2">
{result.combined_obligations.map((obl, i) => (
<div key={i} className="flex items-start gap-2 text-sm">
<svg className="w-4 h-4 text-purple-500 mt-0.5 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<span className="text-gray-700">{obl}</span>
</div>
))}
</div>
</div>
)}
{/* GPAI-specific obligations */}
{result.gpai_result.is_gpai && result.gpai_result.obligations.length > 0 && (
<div className="bg-blue-50 rounded-xl border border-blue-200 p-6">
<h4 className="text-sm font-semibold text-blue-900 mb-3">
GPAI-spezifische Pflichten ({result.gpai_result.obligations.length})
</h4>
<div className="space-y-2">
{result.gpai_result.obligations.map((obl, i) => (
<div key={i} className="flex items-start gap-2 text-sm">
<svg className="w-4 h-4 text-blue-500 mt-0.5 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M11.25 11.25l.041-.02a.75.75 0 011.063.852l-.708 2.836a.75.75 0 001.063.853l.041-.021M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-9-3.75h.008v.008H12V8.25z" />
</svg>
<span className="text-blue-800">{obl}</span>
</div>
))}
</div>
</div>
)}
{/* Answer Summary */}
<div className="bg-white rounded-xl border border-gray-200 p-6">
<h4 className="text-sm font-semibold text-gray-900 mb-3">Ihre Antworten</h4>
<div className="space-y-2">
{definition?.questions.map(q => {
const answer = result.answers[q.id]
if (!answer) return null
return (
<div key={q.id} className="flex items-center gap-3 text-sm py-1.5 border-b border-gray-100 last:border-0">
<span className="text-xs font-mono text-gray-400 w-8">{q.id}</span>
<span className="flex-1 text-gray-600">{q.question}</span>
<span className={`px-2 py-0.5 rounded text-xs font-medium ${
answer.value ? 'bg-green-100 text-green-700' : 'bg-gray-100 text-gray-500'
}`}>
{answer.value ? 'Ja' : 'Nein'}
</span>
</div>
)
})}
</div>
</div>
</div>
)
}
// =========================================================================
// QUESTIONS PHASE
// =========================================================================
return (
<div className="space-y-6">
{/* Progress */}
<div className="bg-white rounded-xl border border-gray-200 p-4">
<div className="flex items-center justify-between mb-3">
<span className="text-sm font-medium text-gray-700">
{systemName} Frage {currentIdx + 1} von {totalVisible}
</span>
<span className={`px-2 py-1 text-xs rounded-full font-medium ${
currentQuestion?.axis === 'high_risk'
? 'bg-orange-100 text-orange-700'
: 'bg-blue-100 text-blue-700'
}`}>
{currentQuestion?.axis === 'high_risk' ? 'High-Risk' : 'GPAI'}
</span>
</div>
{/* Dual progress bar */}
<div className="flex gap-2">
<div className="flex-1">
<div className="text-[10px] text-orange-600 mb-1 font-medium">
Achse 1: High-Risk ({highRiskQuestions.filter(q => answers[q.id] !== undefined).length}/{highRiskQuestions.length})
</div>
<div className="h-1.5 bg-gray-100 rounded-full overflow-hidden">
<div
className="h-full bg-orange-500 rounded-full transition-all"
style={{ width: `${highRiskQuestions.length ? (highRiskQuestions.filter(q => answers[q.id] !== undefined).length / highRiskQuestions.length) * 100 : 0}%` }}
/>
</div>
</div>
<div className="flex-1">
<div className="text-[10px] text-blue-600 mb-1 font-medium">
Achse 2: GPAI ({gpaiQuestions.filter(q => answers[q.id] !== undefined).length}/{gpaiQuestions.length})
</div>
<div className="h-1.5 bg-gray-100 rounded-full overflow-hidden">
<div
className="h-full bg-blue-500 rounded-full transition-all"
style={{ width: `${gpaiQuestions.length ? (gpaiQuestions.filter(q => answers[q.id] !== undefined).length / gpaiQuestions.length) * 100 : 0}%` }}
/>
</div>
</div>
</div>
</div>
{/* Error */}
{error && (
<div className="p-4 bg-red-50 border border-red-200 rounded-lg text-red-700 flex items-center justify-between">
<span>{error}</span>
<button onClick={() => setError(null)} className="text-red-500 hover:text-red-700">&times;</button>
</div>
)}
{/* Current Question */}
{currentQuestion && (
<div className="bg-white rounded-xl border border-gray-200 p-6">
<div className="flex items-start gap-3 mb-4">
<span className="px-2 py-1 text-xs font-mono bg-gray-100 text-gray-500 rounded">{currentQuestion.id}</span>
<span className="px-2 py-1 text-xs bg-purple-50 text-purple-700 rounded">{currentQuestion.article_ref}</span>
</div>
<h3 className="text-lg font-semibold text-gray-900 mb-3">{currentQuestion.question}</h3>
<p className="text-sm text-gray-500 mb-6">{currentQuestion.description}</p>
{/* Answer buttons */}
<div className="grid grid-cols-2 gap-4">
<button
onClick={() => handleAnswer(true)}
className={`p-4 rounded-xl border-2 transition-all text-center font-medium ${
answers[currentQuestion.id]?.value === true
? 'border-green-500 bg-green-50 text-green-700'
: 'border-gray-200 hover:border-green-300 hover:bg-green-50/50 text-gray-700'
}`}
>
<svg className="w-8 h-8 mx-auto mb-2 text-green-500" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
Ja
</button>
<button
onClick={() => handleAnswer(false)}
className={`p-4 rounded-xl border-2 transition-all text-center font-medium ${
answers[currentQuestion.id]?.value === false
? 'border-gray-500 bg-gray-50 text-gray-700'
: 'border-gray-200 hover:border-gray-300 hover:bg-gray-50/50 text-gray-700'
}`}
>
<svg className="w-8 h-8 mx-auto mb-2 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M9.75 9.75l4.5 4.5m0-4.5l-4.5 4.5M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
Nein
</button>
</div>
</div>
)}
{/* Navigation */}
<div className="flex items-center justify-between">
<button
onClick={currentIdx === 0 ? () => setPhase('intro') : handleBack}
className="px-4 py-2 text-sm text-gray-600 hover:bg-gray-100 rounded-lg transition-colors"
>
Zurück
</button>
<div className="flex items-center gap-1">
{visibleQuestions.map((q, i) => (
<button
key={q.id}
onClick={() => setCurrentIdx(i)}
className={`w-2.5 h-2.5 rounded-full transition-colors ${
i === currentIdx
? q.axis === 'high_risk' ? 'bg-orange-500' : 'bg-blue-500'
: answers[q.id] !== undefined
? 'bg-green-400'
: 'bg-gray-200'
}`}
title={`${q.id}: ${q.question}`}
/>
))}
</div>
{allAnswered ? (
<button
onClick={handleSubmit}
disabled={saving}
className={`px-6 py-2 rounded-lg font-medium transition-colors ${
saving
? 'bg-purple-300 text-white cursor-wait'
: 'bg-purple-600 text-white hover:bg-purple-700'
}`}
>
{saving ? (
<span className="flex items-center gap-2">
<svg className="w-4 h-4 animate-spin" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
</svg>
Auswertung...
</span>
) : (
'Auswerten'
)}
</button>
) : (
<button
onClick={() => setCurrentIdx(prev => Math.min(prev + 1, totalVisible - 1))}
disabled={currentIdx >= totalVisible - 1}
className="px-4 py-2 text-sm text-purple-600 hover:bg-purple-50 rounded-lg transition-colors disabled:opacity-30"
>
Weiter
</button>
)}
</div>
</div>
)
}

View File

@@ -10,6 +10,8 @@ interface AssessmentResult {
dsfa_recommended: boolean
art22_risk: boolean
training_allowed: string
betrvg_conflict_score?: number
betrvg_consultation_required?: boolean
summary: string
recommendation: string
alternative_approach?: string
@@ -76,6 +78,21 @@ export function AssessmentResultCard({ result }: AssessmentResultCardProps) {
Art. 22 Risiko
</span>
)}
{result.betrvg_conflict_score != null && result.betrvg_conflict_score > 0 && (
<span className={`px-3 py-1 rounded-full text-sm font-medium ${
result.betrvg_conflict_score >= 75 ? 'bg-red-100 text-red-700' :
result.betrvg_conflict_score >= 50 ? 'bg-orange-100 text-orange-700' :
result.betrvg_conflict_score >= 25 ? 'bg-yellow-100 text-yellow-700' :
'bg-green-100 text-green-700'
}`}>
BR-Konflikt: {result.betrvg_conflict_score}/100
</span>
)}
{result.betrvg_consultation_required && (
<span className="px-3 py-1 rounded-full text-sm bg-purple-100 text-purple-700">
BR-Konsultation erforderlich
</span>
)}
</div>
<p className="text-gray-700">{result.summary}</p>
<p className="text-sm text-gray-500 mt-2">{result.recommendation}</p>

View File

@@ -0,0 +1,53 @@
-- Wiki Article: BetrVG & KI — Mitbestimmung bei IT-Systemen
-- Kategorie: arbeitsrecht (existiert bereits)
-- Ausfuehren auf Production-DB nach Compliance-Refactoring
INSERT INTO compliance.compliance_wiki_articles (id, category_id, title, summary, content, legal_refs, tags, relevance, source_urls, version)
VALUES
('betrvg-ki-mitbestimmung', 'arbeitsrecht',
'BetrVG & KI — Mitbestimmung bei IT-Systemen',
'Uebersicht der Mitbestimmungsrechte des Betriebsrats bei Einfuehrung von KI- und IT-Systemen gemaess §87 Abs.1 Nr.6 BetrVG. Inkl. BAG-Rechtsprechung und Konflikt-Score.',
'# BetrVG & KI — Mitbestimmung bei IT-Systemen
## Kernregel: §87 Abs.1 Nr.6 BetrVG
Die **Einfuehrung und Anwendung** von technischen Einrichtungen, die dazu **geeignet** sind, das Verhalten oder die Leistung der Arbeitnehmer zu ueberwachen, beduerfen der **Zustimmung des Betriebsrats**.
### Wichtig: Eignung genuegt!
Das BAG hat klargestellt: Bereits die **objektive Eignung** zur Ueberwachung genuegt — eine tatsaechliche Nutzung zu diesem Zweck ist nicht erforderlich.
---
## Leitentscheidungen des BAG
### Microsoft Office 365 (BAG 1 ABR 20/21, 08.03.2022)
Das BAG hat ausdruecklich entschieden, dass Microsoft Office 365 der Mitbestimmung unterliegt.
### Standardsoftware (BAG 1 ABN 36/18, 23.10.2018)
Auch alltaegliche Standardsoftware wie Excel ist mitbestimmungsrelevant. Keine Geringfuegigkeitsschwelle.
### SAP ERP (BAG 1 ABR 45/11, 25.09.2012)
HR-/ERP-Systeme erheben und verknuepfen individualisierbare Verhaltens- und Leistungsdaten.
### SaaS/Cloud (BAG 1 ABR 68/13, 21.07.2015)
Auch bei Ueberwachung ueber Dritt-Systeme bleibt der Betriebsrat zu beteiligen.
### Belastungsstatistik (BAG 1 ABR 46/15, 25.04.2017)
Dauerhafte Kennzahlenueberwachung ist ein schwerwiegender Eingriff in das Persoenlichkeitsrecht.
---
## Betriebsrats-Konflikt-Score (SDK)
Das SDK berechnet automatisch einen Konflikt-Score (0-100):
- Beschaeftigtendaten (+10), Ueberwachungseignung (+20), HR-Bezug (+20)
- Individualisierbare Logs (+15), Kommunikationsanalyse (+10)
- Scoring/Ranking (+10), Vollautomatisiert (+10), Keine BR-Konsultation (+5)
Eskalation: Score >= 50 ohne BR → E2, Score >= 75 → E3.',
'["§87 Abs.1 Nr.6 BetrVG", "§90 BetrVG", "§94 BetrVG", "§95 BetrVG", "Art. 88 DSGVO", "§26 BDSG"]',
ARRAY['BetrVG', 'Mitbestimmung', 'Betriebsrat', 'KI', 'Ueberwachung', 'Microsoft 365'],
'critical',
'["https://www.bundesarbeitsgericht.de/entscheidung/1-abr-20-21/", "https://www.bundesarbeitsgericht.de/entscheidung/1-abn-36-18/"]',
1)
ON CONFLICT (id) DO UPDATE SET content = EXCLUDED.content, summary = EXCLUDED.summary, updated_at = NOW();

View File

@@ -0,0 +1,157 @@
-- Wiki Articles: Domain-spezifische KI-Compliance
-- 4 Artikel fuer die wichtigsten Hochrisiko-Domains
-- 1. KI im Recruiting
INSERT INTO compliance.compliance_wiki_articles (id, category_id, title, summary, content, legal_refs, tags, relevance, source_urls, version)
VALUES ('ki-recruiting-compliance', 'arbeitsrecht',
'KI im Recruiting — AGG, DSGVO Art. 22, AI Act Hochrisiko',
'Compliance-Anforderungen bei KI-gestuetzter Personalauswahl: Automatisierte Absagen, Bias-Risiken, Beweislastumkehr.',
'# KI im Recruiting — Compliance-Anforderungen
## AI Act Einstufung
KI im Recruiting faellt unter **Annex III Nr. 4 (Employment)** = **High-Risk**.
## Kritische Punkte
### Art. 22 DSGVO — Automatisierte Entscheidungen
Vollautomatische Absagen ohne menschliche Pruefung sind **grundsaetzlich unzulaessig**.
Erlaubt: KI erstellt Vorschlag → Mensch prueft → Mensch entscheidet → Mensch gibt Absage frei.
### AGG — Diskriminierungsverbot
- § 1 AGG: Keine Benachteiligung nach Geschlecht, Alter, Herkunft, Religion, Behinderung
- § 22 AGG: **Beweislastumkehr** — Arbeitgeber muss beweisen, dass KEINE Diskriminierung vorliegt
- § 15 AGG: Schadensersatz bis 3 Monatsgehaelter pro Fall
- Proxy-Merkmale vermeiden: Name→Herkunft, Foto→Alter
### BetrVG — Mitbestimmung
- § 87 Abs. 1 Nr. 6: Betriebsrat muss zustimmen
- § 95: Auswahlrichtlinien mitbestimmungspflichtig
- BAG 1 ABR 20/21: Gilt auch fuer Standardsoftware
## Pflichtmassnahmen
1. Human-in-the-Loop (echt, kein Rubber Stamping)
2. Regelmaessige Bias-Audits
3. DSFA durchfuehren
4. Betriebsvereinbarung abschliessen
5. Bewerber ueber KI-Nutzung informieren',
'["Art. 22 DSGVO", "§ 1 AGG", "§ 22 AGG", "§ 15 AGG", "§ 87 BetrVG", "§ 95 BetrVG", "Annex III Nr. 4 AI Act"]',
ARRAY['Recruiting', 'HR', 'AGG', 'Bias', 'Art. 22', 'Beweislastumkehr', 'Betriebsrat'],
'critical',
'["https://www.bundesarbeitsgericht.de/entscheidung/1-abr-20-21/"]',
1)
ON CONFLICT (id) DO UPDATE SET content = EXCLUDED.content, updated_at = NOW();
-- 2. KI in der Bildung
INSERT INTO compliance.compliance_wiki_articles (id, category_id, title, summary, content, legal_refs, tags, relevance, source_urls, version)
VALUES ('ki-bildung-compliance', 'branchenspezifisch',
'KI in der Bildung — Notenvergabe, Pruefungsbewertung, Minderjaehrige',
'AI Act Annex III Nr. 3: Hochrisiko bei KI-gestuetzter Bewertung in Bildung und Ausbildung.',
'# KI in der Bildung — Compliance-Anforderungen
## AI Act Einstufung
KI in Bildung/Ausbildung faellt unter **Annex III Nr. 3 (Education)** = **High-Risk**.
## Kritische Szenarien
- KI beeinflusst Noten → High-Risk
- KI bewertet Pruefungen → High-Risk
- KI steuert Zugang zu Bildungsangeboten → High-Risk
- Minderjaehrige betroffen → Besonderer Schutz (Art. 24 EU-Grundrechtecharta)
## BLOCK-Regel
**Minderjaehrige betroffen + keine Lehrkraft-Pruefung = UNZULAESSIG**
## Pflichtmassnahmen
1. Lehrkraft prueft JEDES KI-Ergebnis vor Mitteilung an Schueler
2. Chancengleichheit unabhaengig von sozioekonomischem Hintergrund
3. Keine Benachteiligung durch Sprache oder Behinderung
4. FRIA durchfuehren (Grundrechte-Folgenabschaetzung)
5. DSFA bei Verarbeitung von Schuelerdaten
## Grundrechte
- Recht auf Bildung (Art. 14 EU-Charta)
- Rechte des Kindes (Art. 24 EU-Charta)
- Nicht-Diskriminierung (Art. 21 EU-Charta)',
'["Annex III Nr. 3 AI Act", "Art. 14 EU-Grundrechtecharta", "Art. 24 EU-Grundrechtecharta", "Art. 35 DSGVO"]',
ARRAY['Bildung', 'Education', 'Noten', 'Pruefung', 'Minderjaehrige', 'Schule'],
'critical',
'[]',
1)
ON CONFLICT (id) DO UPDATE SET content = EXCLUDED.content, updated_at = NOW();
-- 3. KI im Gesundheitswesen
INSERT INTO compliance.compliance_wiki_articles (id, category_id, title, summary, content, legal_refs, tags, relevance, source_urls, version)
VALUES ('ki-gesundheit-compliance', 'branchenspezifisch',
'KI im Gesundheitswesen — MDR, Diagnose, Triage',
'AI Act Annex III Nr. 5 + MDR: Hochrisiko bei KI in Diagnose, Behandlung und Triage.',
'# KI im Gesundheitswesen — Compliance-Anforderungen
## Regulatorischer Rahmen
- **AI Act Annex III Nr. 5** — Zugang zu wesentlichen Diensten (Gesundheit)
- **MDR (EU) 2017/745** — Medizinprodukteverordnung
- **DSGVO Art. 9** — Gesundheitsdaten = besondere Kategorie
## Kritische Szenarien
- KI unterstuetzt Diagnosen → High-Risk + DSFA Pflicht
- KI priorisiert Patienten (Triage) → Lebenskritisch, hoechste Anforderungen
- KI empfiehlt Behandlungen → High-Risk
- System ist Medizinprodukt → MDR-Zertifizierung erforderlich
## BLOCK-Regeln
- **Medizinprodukt ohne klinische Validierung = UNZULAESSIG**
- MDR Art. 61: Klinische Bewertung ist Pflicht
## Grundrechte
- Menschenwuerde (Art. 1 EU-Charta)
- Schutz personenbezogener Daten (Art. 8 EU-Charta)
- Patientenautonomie
## Pflichtmassnahmen
1. Klinische Validierung vor Einsatz
2. Human Oversight durch qualifiziertes Fachpersonal
3. DSFA fuer Gesundheitsdatenverarbeitung
4. Genauigkeitsmetriken definieren und messen
5. Incident Reporting bei Fehlfunktionen',
'["Annex III Nr. 5 AI Act", "MDR (EU) 2017/745", "Art. 9 DSGVO", "Art. 35 DSGVO"]',
ARRAY['Gesundheit', 'Healthcare', 'MDR', 'Diagnose', 'Triage', 'Medizinprodukt'],
'critical',
'[]',
1)
ON CONFLICT (id) DO UPDATE SET content = EXCLUDED.content, updated_at = NOW();
-- 4. KI in Finanzdienstleistungen
INSERT INTO compliance.compliance_wiki_articles (id, category_id, title, summary, content, legal_refs, tags, relevance, source_urls, version)
VALUES ('ki-finance-compliance', 'branchenspezifisch',
'KI in Finanzdienstleistungen — Scoring, DORA, Versicherung',
'AI Act Annex III Nr. 5 + DORA + MaRisk: Compliance bei Kredit-Scoring, Algo-Trading, Versicherungspraemien.',
'# KI in Finanzdienstleistungen — Compliance-Anforderungen
## Regulatorischer Rahmen
- **AI Act Annex III Nr. 5** — Zugang zu wesentlichen Diensten
- **DORA** — Digital Operational Resilience Act
- **MaRisk/BAIT** — Bankaufsichtliche Anforderungen
- **MiFID II** — Algorithmischer Handel
## Kritische Szenarien
- Kredit-Scoring → High-Risk (Art. 22 DSGVO + Annex III)
- Automatisierte Schadenbearbeitung → Art. 22 Risiko
- Individuelle Praemienberechnung → Diskriminierungsrisiko
- Algo-Trading → MiFID II Anforderungen
- Robo Advisor → WpHG-Pflichten
## Pflichtmassnahmen
1. Transparenz bei Scoring-Entscheidungen
2. Bias-Audits bei Kreditvergabe
3. Human Oversight bei Ablehnungen
4. DORA-konforme IT-Resilienz
5. Incident Reporting
## Besondere Risiken
- Diskriminierendes Kredit-Scoring (AGG + AI Act)
- Ungerechtfertigte Verweigerung von Finanzdienstleistungen
- Mangelnde Erklaerbarkeit bei Scoring-Algorithmen',
'["Annex III Nr. 5 AI Act", "DORA", "MaRisk", "MiFID II", "Art. 22 DSGVO", "§ 1 AGG"]',
ARRAY['Finance', 'Banking', 'Versicherung', 'Scoring', 'DORA', 'Kredit', 'Algo-Trading'],
'critical',
'[]',
1)
ON CONFLICT (id) DO UPDATE SET content = EXCLUDED.content, updated_at = NOW();

View File

@@ -104,6 +104,10 @@ func main() {
auditHandlers := handlers.NewAuditHandlers(auditStore, exporter)
uccaHandlers := handlers.NewUCCAHandlers(uccaStore, escalationStore, providerRegistry)
escalationHandlers := handlers.NewEscalationHandlers(escalationStore, uccaStore)
registrationStore := ucca.NewRegistrationStore(pool)
registrationHandlers := handlers.NewRegistrationHandlers(registrationStore, uccaStore)
paymentHandlers := handlers.NewPaymentHandlers(pool)
tenderHandlers := handlers.NewTenderHandlers(pool, paymentHandlers.GetControlLibrary())
roadmapHandlers := handlers.NewRoadmapHandlers(roadmapStore)
workshopHandlers := handlers.NewWorkshopHandlers(workshopStore)
portfolioHandlers := handlers.NewPortfolioHandlers(portfolioStore)
@@ -270,10 +274,49 @@ func main() {
uccaRoutes.POST("/escalations/:id/review", escalationHandlers.StartReview)
uccaRoutes.POST("/escalations/:id/decide", escalationHandlers.DecideEscalation)
// AI Act Decision Tree
dtRoutes := uccaRoutes.Group("/decision-tree")
{
dtRoutes.GET("", uccaHandlers.GetDecisionTree)
dtRoutes.POST("/evaluate", uccaHandlers.EvaluateDecisionTree)
dtRoutes.GET("/results", uccaHandlers.ListDecisionTreeResults)
dtRoutes.GET("/results/:id", uccaHandlers.GetDecisionTreeResult)
dtRoutes.DELETE("/results/:id", uccaHandlers.DeleteDecisionTreeResult)
}
// Obligations framework (v2 with TOM mapping)
obligationsHandlers.RegisterRoutes(uccaRoutes)
}
// AI Registration routes - EU AI Database (Art. 49)
regRoutes := v1.Group("/ai-registration")
{
regRoutes.POST("", registrationHandlers.Create)
regRoutes.GET("", registrationHandlers.List)
regRoutes.GET("/:id", registrationHandlers.Get)
regRoutes.PUT("/:id", registrationHandlers.Update)
regRoutes.PATCH("/:id/status", registrationHandlers.UpdateStatus)
regRoutes.POST("/prefill/:assessment_id", registrationHandlers.Prefill)
regRoutes.GET("/:id/export", registrationHandlers.Export)
}
// Payment Compliance routes
payRoutes := v1.Group("/payment-compliance")
{
payRoutes.GET("/controls", paymentHandlers.ListControls)
payRoutes.POST("/assessments", paymentHandlers.CreateAssessment)
payRoutes.GET("/assessments", paymentHandlers.ListAssessments)
payRoutes.GET("/assessments/:id", paymentHandlers.GetAssessment)
payRoutes.PATCH("/assessments/:id/verdict", paymentHandlers.UpdateControlVerdict)
// Tender Analysis
payRoutes.POST("/tender/upload", tenderHandlers.Upload)
payRoutes.POST("/tender/:id/extract", tenderHandlers.Extract)
payRoutes.POST("/tender/:id/match", tenderHandlers.Match)
payRoutes.GET("/tender", tenderHandlers.ListAnalyses)
payRoutes.GET("/tender/:id", tenderHandlers.GetAnalysis)
}
// RAG routes - Legal Corpus Search & Versioning
ragRoutes := v1.Group("/rag")
{

View 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})
}

View 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(&reg); 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(), &reg); 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)
}

View 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
}

View File

@@ -1122,6 +1122,114 @@ func (h *UCCAHandlers) GetWizardSchema(c *gin.Context) {
})
}
// ============================================================================
// AI Act Decision Tree Endpoints
// ============================================================================
// GetDecisionTree returns the decision tree structure for the frontend
// GET /sdk/v1/ucca/decision-tree
func (h *UCCAHandlers) GetDecisionTree(c *gin.Context) {
tree := ucca.BuildDecisionTreeDefinition()
c.JSON(http.StatusOK, tree)
}
// EvaluateDecisionTree evaluates the decision tree answers and stores the result
// POST /sdk/v1/ucca/decision-tree/evaluate
func (h *UCCAHandlers) EvaluateDecisionTree(c *gin.Context) {
tenantID := rbac.GetTenantID(c)
if tenantID == uuid.Nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "tenant ID required"})
return
}
var req ucca.DecisionTreeEvalRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if req.SystemName == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "system_name is required"})
return
}
// Evaluate
result := ucca.EvaluateDecisionTree(&req)
result.TenantID = tenantID
// Parse optional project_id
if projectIDStr := c.Query("project_id"); projectIDStr != "" {
if pid, err := uuid.Parse(projectIDStr); err == nil {
result.ProjectID = &pid
}
}
// Store result
if err := h.store.CreateDecisionTreeResult(c.Request.Context(), result); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusCreated, result)
}
// ListDecisionTreeResults returns stored decision tree results for a tenant
// GET /sdk/v1/ucca/decision-tree/results
func (h *UCCAHandlers) ListDecisionTreeResults(c *gin.Context) {
tenantID := rbac.GetTenantID(c)
if tenantID == uuid.Nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "tenant ID required"})
return
}
results, err := h.store.ListDecisionTreeResults(c.Request.Context(), tenantID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"results": results, "total": len(results)})
}
// GetDecisionTreeResult returns a single decision tree result by ID
// GET /sdk/v1/ucca/decision-tree/results/:id
func (h *UCCAHandlers) GetDecisionTreeResult(c *gin.Context) {
id, err := uuid.Parse(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid ID"})
return
}
result, err := h.store.GetDecisionTreeResult(c.Request.Context(), id)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
if result == nil {
c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
return
}
c.JSON(http.StatusOK, result)
}
// DeleteDecisionTreeResult deletes a decision tree result
// DELETE /sdk/v1/ucca/decision-tree/results/:id
func (h *UCCAHandlers) DeleteDecisionTreeResult(c *gin.Context) {
id, err := uuid.Parse(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid ID"})
return
}
if err := h.store.DeleteDecisionTreeResult(c.Request.Context(), id); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "deleted"})
}
// ============================================================================
// Helper functions
// ============================================================================

View 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")
}
}

View File

@@ -0,0 +1,325 @@
package ucca
// ============================================================================
// AI Act Decision Tree Engine
// ============================================================================
//
// Two-axis classification:
// Axis 1 (Q1Q7): High-Risk classification based on Annex III
// Axis 2 (Q8Q12): GPAI classification based on Art. 5156
//
// 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. 34",
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. 68",
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. 5156) ===
{
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 Q1Q7
func evaluateHighRiskAxis(answers map[string]DecisionTreeAnswer) AIActRiskLevel {
// Q1: Uses AI at all?
if !answerIsYes(answers, Q1) {
return AIActNotApplicable
}
// Q2Q6: 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 Q8Q12
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
}

View 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)
}
})
}
}

View 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")
}
}
}

View File

@@ -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

View File

@@ -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
}

View File

@@ -217,10 +217,221 @@ type UseCaseIntake struct {
// Only applicable for financial domains (banking, finance, insurance, investment)
FinancialContext *FinancialContext `json:"financial_context,omitempty"`
// BetrVG / works council context (Germany)
EmployeeMonitoring bool `json:"employee_monitoring,omitempty"` // System can monitor employee behavior/performance
HRDecisionSupport bool `json:"hr_decision_support,omitempty"` // System supports HR decisions (hiring, evaluation, termination)
WorksCouncilConsulted bool `json:"works_council_consulted,omitempty"` // Works council has been consulted
// Domain-specific contexts (AI Act Annex III high-risk domains)
HRContext *HRContext `json:"hr_context,omitempty"`
EducationContext *EducationContext `json:"education_context,omitempty"`
HealthcareContext *HealthcareContext `json:"healthcare_context,omitempty"`
LegalDomainContext *LegalDomainContext `json:"legal_context,omitempty"`
PublicSectorContext *PublicSectorContext `json:"public_sector_context,omitempty"`
CriticalInfraContext *CriticalInfraContext `json:"critical_infra_context,omitempty"`
AutomotiveContext *AutomotiveContext `json:"automotive_context,omitempty"`
RetailContext *RetailContext `json:"retail_context,omitempty"`
ITSecurityContext *ITSecurityContext `json:"it_security_context,omitempty"`
LogisticsContext *LogisticsContext `json:"logistics_context,omitempty"`
ConstructionContext *ConstructionContext `json:"construction_context,omitempty"`
MarketingContext *MarketingContext `json:"marketing_context,omitempty"`
ManufacturingContext *ManufacturingContext `json:"manufacturing_context,omitempty"`
AgricultureContext *AgricultureContext `json:"agriculture_context,omitempty"`
SocialServicesCtx *SocialServicesContext `json:"social_services_context,omitempty"`
HospitalityContext *HospitalityContext `json:"hospitality_context,omitempty"`
InsuranceContext *InsuranceContext `json:"insurance_context,omitempty"`
InvestmentContext *InvestmentContext `json:"investment_context,omitempty"`
DefenseContext *DefenseContext `json:"defense_context,omitempty"`
SupplyChainContext *SupplyChainContext `json:"supply_chain_context,omitempty"`
FacilityContext *FacilityContext `json:"facility_context,omitempty"`
SportsContext *SportsContext `json:"sports_context,omitempty"`
// Opt-in to store raw text (otherwise only hash)
StoreRawText bool `json:"store_raw_text,omitempty"`
}
// HRContext captures HR/recruiting-specific compliance data (AI Act Annex III Nr. 4 + AGG)
type HRContext struct {
AutomatedScreening bool `json:"automated_screening"` // KI sortiert Bewerber vor
AutomatedRejection bool `json:"automated_rejection"` // KI generiert Absagen
CandidateRanking bool `json:"candidate_ranking"` // KI erstellt Bewerber-Rankings
BiasAuditsDone bool `json:"bias_audits_done"` // Regelmaessige Bias-Audits
AGGCategoriesVisible bool `json:"agg_categories_visible"` // System kann Name/Foto/Alter erkennen
HumanReviewEnforced bool `json:"human_review_enforced"` // Mensch prueft jede KI-Empfehlung
PerformanceEvaluation bool `json:"performance_evaluation"` // KI bewertet Mitarbeiterleistung
}
// EducationContext captures education-specific compliance data (AI Act Annex III Nr. 3)
type EducationContext struct {
GradeInfluence bool `json:"grade_influence"` // KI beeinflusst Noten
ExamEvaluation bool `json:"exam_evaluation"` // KI bewertet Pruefungen
StudentSelection bool `json:"student_selection"` // KI beeinflusst Zugang/Auswahl
MinorsInvolved bool `json:"minors_involved"` // Minderjaehrige betroffen
TeacherReviewRequired bool `json:"teacher_review_required"` // Lehrkraft prueft KI-Ergebnis
LearningAdaptation bool `json:"learning_adaptation"` // KI passt Lernpfade an
}
// HealthcareContext captures healthcare-specific compliance data (AI Act Annex III Nr. 5 + MDR)
type HealthcareContext struct {
DiagnosisSupport bool `json:"diagnosis_support"` // KI unterstuetzt Diagnosen
TreatmentRecommend bool `json:"treatment_recommendation"` // KI empfiehlt Behandlungen
TriageDecision bool `json:"triage_decision"` // KI priorisiert Patienten
PatientDataProcessed bool `json:"patient_data_processed"` // Gesundheitsdaten verarbeitet
MedicalDevice bool `json:"medical_device"` // System ist Medizinprodukt
ClinicalValidation bool `json:"clinical_validation"` // Klinisch validiert
}
// LegalDomainContext captures legal/justice-specific compliance data (AI Act Annex III Nr. 8)
type LegalDomainContext struct {
LegalAdvice bool `json:"legal_advice"` // KI gibt Rechtsberatung
ContractAnalysis bool `json:"contract_analysis"` // KI analysiert Vertraege
CourtPrediction bool `json:"court_prediction"` // KI prognostiziert Urteile
AccessToJustice bool `json:"access_to_justice"` // KI beeinflusst Zugang zu Recht
ClientConfidential bool `json:"client_confidential"` // Mandantengeheimnis betroffen
}
// PublicSectorContext captures public sector compliance data (Art. 27 FRIA)
type PublicSectorContext struct {
AdminDecision bool `json:"admin_decision"` // KI beeinflusst Verwaltungsentscheidungen
CitizenService bool `json:"citizen_service"` // KI in Buergerservices
BenefitAllocation bool `json:"benefit_allocation"` // KI verteilt Leistungen/Mittel
PublicSafety bool `json:"public_safety"` // KI in oeffentlicher Sicherheit
TransparencyEnsured bool `json:"transparency_ensured"` // Transparenz gegenueber Buergern
}
// CriticalInfraContext captures critical infrastructure data (NIS2 + Annex III Nr. 2)
type CriticalInfraContext struct {
GridControl bool `json:"grid_control"` // KI steuert Netz/Infrastruktur
SafetyCritical bool `json:"safety_critical"` // Sicherheitskritische Steuerung
AnomalyDetection bool `json:"anomaly_detection"` // KI erkennt Anomalien
RedundancyExists bool `json:"redundancy_exists"` // Redundante Systeme vorhanden
IncidentResponse bool `json:"incident_response"` // Incident Response Plan vorhanden
}
// AutomotiveContext captures automotive/aerospace safety data
type AutomotiveContext struct {
AutonomousDriving bool `json:"autonomous_driving"` // Autonomes Fahren / ADAS
SafetyRelevant bool `json:"safety_relevant"` // Sicherheitsrelevante Funktion
TypeApprovalNeeded bool `json:"type_approval_needed"` // Typgenehmigung erforderlich
FunctionalSafety bool `json:"functional_safety"` // ISO 26262 relevant
}
// RetailContext captures retail/e-commerce compliance data
type RetailContext struct {
PricingPersonalized bool `json:"pricing_personalized"` // Personalisierte Preise
CustomerProfiling bool `json:"customer_profiling"` // Kundenprofilbildung
RecommendationEngine bool `json:"recommendation_engine"` // Empfehlungssystem
CreditScoring bool `json:"credit_scoring"` // Bonitaetspruefung bei Kauf
DarkPatterns bool `json:"dark_patterns"` // Manipulative UI-Muster moeglich
}
// ITSecurityContext captures IT/cybersecurity/telecom data
type ITSecurityContext struct {
EmployeeSurveillance bool `json:"employee_surveillance"` // Mitarbeiterueberwachung
NetworkMonitoring bool `json:"network_monitoring"` // Netzwerkueberwachung
ThreatDetection bool `json:"threat_detection"` // Bedrohungserkennung
AccessControl bool `json:"access_control_ai"` // KI-gestuetzte Zugriffskontrolle
DataRetention bool `json:"data_retention_logs"` // Umfangreiche Log-Speicherung
}
// LogisticsContext captures logistics/transport compliance data
type LogisticsContext struct {
DriverTracking bool `json:"driver_tracking"` // Fahrer-/Kurier-Tracking
RouteOptimization bool `json:"route_optimization"` // Routenoptimierung mit Personenbezug
WorkloadScoring bool `json:"workload_scoring"` // Leistungsbewertung Lagerarbeiter
PredictiveMaint bool `json:"predictive_maintenance"` // Vorausschauende Wartung
}
// ConstructionContext captures construction/real estate data
type ConstructionContext struct {
SafetyMonitoring bool `json:"safety_monitoring"` // Baustellensicherheit per KI
TenantScreening bool `json:"tenant_screening"` // KI-gestuetzte Mieterauswahl
BuildingAutomation bool `json:"building_automation"` // Gebaeudesteuerung
WorkerSafety bool `json:"worker_safety"` // Arbeitsschutzueberwachung
}
// MarketingContext captures marketing/media compliance data
type MarketingContext struct {
DeepfakeContent bool `json:"deepfake_content"` // Synthetische Inhalte (Deepfakes)
ContentModeration bool `json:"content_moderation"` // Automatische Inhaltsmoderation
BehavioralTargeting bool `json:"behavioral_targeting"` // Verhaltensbasiertes Targeting
MinorsTargeted bool `json:"minors_targeted"` // Minderjaehrige als Zielgruppe
AIContentLabeled bool `json:"ai_content_labeled"` // KI-Inhalte als solche gekennzeichnet
}
// ManufacturingContext captures manufacturing/CE safety data
type ManufacturingContext struct {
MachineSafety bool `json:"machine_safety"` // Maschinensicherheit
QualityControl bool `json:"quality_control"` // KI in Qualitaetskontrolle
ProcessControl bool `json:"process_control"` // KI steuert Fertigungsprozess
CEMarkingRequired bool `json:"ce_marking_required"` // CE-Kennzeichnung erforderlich
SafetyValidated bool `json:"safety_validated"` // Sicherheitsvalidierung durchgefuehrt
}
// AgricultureContext captures agriculture/forestry compliance data
type AgricultureContext struct {
PesticideAI bool `json:"pesticide_ai"` // KI steuert Pestizideinsatz
AnimalWelfare bool `json:"animal_welfare"` // KI beeinflusst Tierhaltung
EnvironmentalData bool `json:"environmental_data"` // Umweltdaten verarbeitet
}
// SocialServicesContext captures social services/nonprofit data
type SocialServicesContext struct {
VulnerableGroups bool `json:"vulnerable_groups"` // Schutzbeduerftiger Personenkreis
BenefitDecision bool `json:"benefit_decision"` // KI beeinflusst Leistungszuteilung
CaseManagement bool `json:"case_management"` // KI in Fallmanagement
}
// HospitalityContext captures hospitality/tourism data
type HospitalityContext struct {
GuestProfiling bool `json:"guest_profiling"` // Gaeste-Profilbildung
DynamicPricing bool `json:"dynamic_pricing"` // Dynamische Preisgestaltung
ReviewManipulation bool `json:"review_manipulation"` // KI beeinflusst Bewertungen
}
// InsuranceContext captures insurance-specific data (beyond FinancialContext)
type InsuranceContext struct {
RiskClassification bool `json:"risk_classification"` // KI klassifiziert Versicherungsrisiken
ClaimsAutomation bool `json:"claims_automation"` // Automatisierte Schadenbearbeitung
PremiumCalculation bool `json:"premium_calculation"` // KI berechnet Praemien individuell
FraudDetection bool `json:"fraud_detection"` // Betrugserkennung
}
// InvestmentContext captures investment-specific data
type InvestmentContext struct {
AlgoTrading bool `json:"algo_trading"` // Algorithmischer Handel
InvestmentAdvice bool `json:"investment_advice"` // KI-gestuetzte Anlageberatung
RoboAdvisor bool `json:"robo_advisor"` // Automatisierte Vermoegensberatung
}
// DefenseContext captures defense/dual-use data
type DefenseContext struct {
DualUse bool `json:"dual_use"` // Dual-Use Technologie
ExportControlled bool `json:"export_controlled"` // Exportkontrolle relevant
ClassifiedData bool `json:"classified_data"` // Verschlusssachen verarbeitet
}
// SupplyChainContext captures textile/packaging/supply chain data (LkSG)
type SupplyChainContext struct {
SupplierMonitoring bool `json:"supplier_monitoring"` // KI ueberwacht Lieferanten
HumanRightsCheck bool `json:"human_rights_check"` // Menschenrechtspruefung in Lieferkette
EnvironmentalImpact bool `json:"environmental_impact"` // Umweltauswirkungen analysiert
}
// FacilityContext captures facility management data
type FacilityContext struct {
AccessControlAI bool `json:"access_control_ai"` // KI-Zutrittskontrolle
OccupancyTracking bool `json:"occupancy_tracking"` // Belegungsueberwachung
EnergyOptimization bool `json:"energy_optimization"` // Energieoptimierung
}
// SportsContext captures sports/general data
type SportsContext struct {
AthleteTracking bool `json:"athlete_tracking"` // Athleten-Performance-Tracking
FanProfiling bool `json:"fan_profiling"` // Fan-/Zuschauer-Profilbildung
DopingDetection bool `json:"doping_detection"` // KI in Doping-Kontrolle
}
// DataTypes specifies what kinds of data are processed
type DataTypes struct {
PersonalData bool `json:"personal_data"`
@@ -383,6 +594,13 @@ type AssessmentResult struct {
Art22Risk bool `json:"art22_risk"` // Art. 22 GDPR automated decision risk
TrainingAllowed TrainingAllowed `json:"training_allowed"`
// BetrVG Conflict Score (0-100) — works council escalation risk
BetrvgConflictScore int `json:"betrvg_conflict_score"`
BetrvgConsultationRequired bool `json:"betrvg_consultation_required"`
// Input (needed for escalation logic)
Intake UseCaseIntake `json:"-"` // not serialized, internal use only
// Summary for humans
Summary string `json:"summary"`
Recommendation string `json:"recommendation"`
@@ -471,6 +689,10 @@ type Assessment struct {
Art22Risk bool `json:"art22_risk"`
TrainingAllowed TrainingAllowed `json:"training_allowed"`
// BetrVG Conflict Score (0-100) — works council escalation risk
BetrvgConflictScore int `json:"betrvg_conflict_score"`
BetrvgConsultationRequired bool `json:"betrvg_consultation_required"`
// Corpus Versioning (RAG)
CorpusVersionID *uuid.UUID `json:"corpus_version_id,omitempty"`
CorpusVersion string `json:"corpus_version,omitempty"`
@@ -525,3 +747,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"`
}

View File

@@ -220,6 +220,7 @@ func (e *PolicyEngine) Evaluate(intake *UseCaseIntake) *AssessmentResult {
RiskLevel: RiskLevelMINIMAL,
Complexity: ComplexityLOW,
RiskScore: 0,
Intake: *intake,
TriggeredRules: []TriggeredRule{},
RequiredControls: []RequiredControl{},
RecommendedArchitecture: []PatternRecommendation{},
@@ -338,6 +339,9 @@ func (e *PolicyEngine) Evaluate(intake *UseCaseIntake) *AssessmentResult {
// Determine complexity
result.Complexity = e.calculateComplexity(result)
// Calculate BetrVG Conflict Score (Germany only, employees >= 5)
result.BetrvgConflictScore, result.BetrvgConsultationRequired = e.calculateBetrvgConflictScore(intake)
// Check if DSFA is recommended
result.DSFARecommended = e.shouldRecommendDSFA(intake, result)
@@ -457,11 +461,382 @@ func (e *PolicyEngine) getFieldValue(field string, intake *UseCaseIntake) interf
return nil
}
return e.getRetentionValue(parts[1], intake)
case "employee_monitoring":
return intake.EmployeeMonitoring
case "hr_decision_support":
return intake.HRDecisionSupport
case "works_council_consulted":
return intake.WorksCouncilConsulted
case "hr_context":
if len(parts) < 2 || intake.HRContext == nil {
return nil
}
return e.getHRContextValue(parts[1], intake)
case "education_context":
if len(parts) < 2 || intake.EducationContext == nil {
return nil
}
return e.getEducationContextValue(parts[1], intake)
case "healthcare_context":
if len(parts) < 2 || intake.HealthcareContext == nil {
return nil
}
return e.getHealthcareContextValue(parts[1], intake)
case "legal_context":
if len(parts) < 2 || intake.LegalDomainContext == nil {
return nil
}
return e.getLegalContextValue(parts[1], intake)
case "public_sector_context":
if len(parts) < 2 || intake.PublicSectorContext == nil {
return nil
}
return e.getPublicSectorContextValue(parts[1], intake)
case "critical_infra_context":
if len(parts) < 2 || intake.CriticalInfraContext == nil {
return nil
}
return e.getCriticalInfraContextValue(parts[1], intake)
case "automotive_context":
if len(parts) < 2 || intake.AutomotiveContext == nil {
return nil
}
return e.getAutomotiveContextValue(parts[1], intake)
case "retail_context":
if len(parts) < 2 || intake.RetailContext == nil {
return nil
}
return e.getRetailContextValue(parts[1], intake)
case "it_security_context":
if len(parts) < 2 || intake.ITSecurityContext == nil {
return nil
}
return e.getITSecurityContextValue(parts[1], intake)
case "logistics_context":
if len(parts) < 2 || intake.LogisticsContext == nil {
return nil
}
return e.getLogisticsContextValue(parts[1], intake)
case "construction_context":
if len(parts) < 2 || intake.ConstructionContext == nil {
return nil
}
return e.getConstructionContextValue(parts[1], intake)
case "marketing_context":
if len(parts) < 2 || intake.MarketingContext == nil {
return nil
}
return e.getMarketingContextValue(parts[1], intake)
case "manufacturing_context":
if len(parts) < 2 || intake.ManufacturingContext == nil {
return nil
}
return e.getManufacturingContextValue(parts[1], intake)
case "agriculture_context":
if len(parts) < 2 || intake.AgricultureContext == nil { return nil }
return e.getAgricultureContextValue(parts[1], intake)
case "social_services_context":
if len(parts) < 2 || intake.SocialServicesCtx == nil { return nil }
return e.getSocialServicesContextValue(parts[1], intake)
case "hospitality_context":
if len(parts) < 2 || intake.HospitalityContext == nil { return nil }
return e.getHospitalityContextValue(parts[1], intake)
case "insurance_context":
if len(parts) < 2 || intake.InsuranceContext == nil { return nil }
return e.getInsuranceContextValue(parts[1], intake)
case "investment_context":
if len(parts) < 2 || intake.InvestmentContext == nil { return nil }
return e.getInvestmentContextValue(parts[1], intake)
case "defense_context":
if len(parts) < 2 || intake.DefenseContext == nil { return nil }
return e.getDefenseContextValue(parts[1], intake)
case "supply_chain_context":
if len(parts) < 2 || intake.SupplyChainContext == nil { return nil }
return e.getSupplyChainContextValue(parts[1], intake)
case "facility_context":
if len(parts) < 2 || intake.FacilityContext == nil { return nil }
return e.getFacilityContextValue(parts[1], intake)
case "sports_context":
if len(parts) < 2 || intake.SportsContext == nil { return nil }
return e.getSportsContextValue(parts[1], intake)
}
return nil
}
func (e *PolicyEngine) getHRContextValue(field string, intake *UseCaseIntake) interface{} {
if intake.HRContext == nil {
return nil
}
switch field {
case "automated_screening":
return intake.HRContext.AutomatedScreening
case "automated_rejection":
return intake.HRContext.AutomatedRejection
case "candidate_ranking":
return intake.HRContext.CandidateRanking
case "bias_audits_done":
return intake.HRContext.BiasAuditsDone
case "agg_categories_visible":
return intake.HRContext.AGGCategoriesVisible
case "human_review_enforced":
return intake.HRContext.HumanReviewEnforced
case "performance_evaluation":
return intake.HRContext.PerformanceEvaluation
}
return nil
}
func (e *PolicyEngine) getEducationContextValue(field string, intake *UseCaseIntake) interface{} {
if intake.EducationContext == nil {
return nil
}
switch field {
case "grade_influence":
return intake.EducationContext.GradeInfluence
case "exam_evaluation":
return intake.EducationContext.ExamEvaluation
case "student_selection":
return intake.EducationContext.StudentSelection
case "minors_involved":
return intake.EducationContext.MinorsInvolved
case "teacher_review_required":
return intake.EducationContext.TeacherReviewRequired
case "learning_adaptation":
return intake.EducationContext.LearningAdaptation
}
return nil
}
func (e *PolicyEngine) getHealthcareContextValue(field string, intake *UseCaseIntake) interface{} {
if intake.HealthcareContext == nil {
return nil
}
switch field {
case "diagnosis_support":
return intake.HealthcareContext.DiagnosisSupport
case "treatment_recommendation":
return intake.HealthcareContext.TreatmentRecommend
case "triage_decision":
return intake.HealthcareContext.TriageDecision
case "patient_data_processed":
return intake.HealthcareContext.PatientDataProcessed
case "medical_device":
return intake.HealthcareContext.MedicalDevice
case "clinical_validation":
return intake.HealthcareContext.ClinicalValidation
}
return nil
}
func (e *PolicyEngine) getLegalContextValue(field string, intake *UseCaseIntake) interface{} {
if intake.LegalDomainContext == nil { return nil }
switch field {
case "legal_advice": return intake.LegalDomainContext.LegalAdvice
case "contract_analysis": return intake.LegalDomainContext.ContractAnalysis
case "court_prediction": return intake.LegalDomainContext.CourtPrediction
case "access_to_justice": return intake.LegalDomainContext.AccessToJustice
case "client_confidential": return intake.LegalDomainContext.ClientConfidential
}
return nil
}
func (e *PolicyEngine) getPublicSectorContextValue(field string, intake *UseCaseIntake) interface{} {
if intake.PublicSectorContext == nil { return nil }
switch field {
case "admin_decision": return intake.PublicSectorContext.AdminDecision
case "citizen_service": return intake.PublicSectorContext.CitizenService
case "benefit_allocation": return intake.PublicSectorContext.BenefitAllocation
case "public_safety": return intake.PublicSectorContext.PublicSafety
case "transparency_ensured": return intake.PublicSectorContext.TransparencyEnsured
}
return nil
}
func (e *PolicyEngine) getCriticalInfraContextValue(field string, intake *UseCaseIntake) interface{} {
if intake.CriticalInfraContext == nil { return nil }
switch field {
case "grid_control": return intake.CriticalInfraContext.GridControl
case "safety_critical": return intake.CriticalInfraContext.SafetyCritical
case "anomaly_detection": return intake.CriticalInfraContext.AnomalyDetection
case "redundancy_exists": return intake.CriticalInfraContext.RedundancyExists
case "incident_response": return intake.CriticalInfraContext.IncidentResponse
}
return nil
}
func (e *PolicyEngine) getAutomotiveContextValue(field string, intake *UseCaseIntake) interface{} {
if intake.AutomotiveContext == nil { return nil }
switch field {
case "autonomous_driving": return intake.AutomotiveContext.AutonomousDriving
case "safety_relevant": return intake.AutomotiveContext.SafetyRelevant
case "type_approval_needed": return intake.AutomotiveContext.TypeApprovalNeeded
case "functional_safety": return intake.AutomotiveContext.FunctionalSafety
}
return nil
}
func (e *PolicyEngine) getRetailContextValue(field string, intake *UseCaseIntake) interface{} {
if intake.RetailContext == nil { return nil }
switch field {
case "pricing_personalized": return intake.RetailContext.PricingPersonalized
case "customer_profiling": return intake.RetailContext.CustomerProfiling
case "recommendation_engine": return intake.RetailContext.RecommendationEngine
case "credit_scoring": return intake.RetailContext.CreditScoring
case "dark_patterns": return intake.RetailContext.DarkPatterns
}
return nil
}
func (e *PolicyEngine) getITSecurityContextValue(field string, intake *UseCaseIntake) interface{} {
if intake.ITSecurityContext == nil { return nil }
switch field {
case "employee_surveillance": return intake.ITSecurityContext.EmployeeSurveillance
case "network_monitoring": return intake.ITSecurityContext.NetworkMonitoring
case "threat_detection": return intake.ITSecurityContext.ThreatDetection
case "access_control_ai": return intake.ITSecurityContext.AccessControl
case "data_retention_logs": return intake.ITSecurityContext.DataRetention
}
return nil
}
func (e *PolicyEngine) getLogisticsContextValue(field string, intake *UseCaseIntake) interface{} {
if intake.LogisticsContext == nil { return nil }
switch field {
case "driver_tracking": return intake.LogisticsContext.DriverTracking
case "route_optimization": return intake.LogisticsContext.RouteOptimization
case "workload_scoring": return intake.LogisticsContext.WorkloadScoring
case "predictive_maintenance": return intake.LogisticsContext.PredictiveMaint
}
return nil
}
func (e *PolicyEngine) getConstructionContextValue(field string, intake *UseCaseIntake) interface{} {
if intake.ConstructionContext == nil { return nil }
switch field {
case "safety_monitoring": return intake.ConstructionContext.SafetyMonitoring
case "tenant_screening": return intake.ConstructionContext.TenantScreening
case "building_automation": return intake.ConstructionContext.BuildingAutomation
case "worker_safety": return intake.ConstructionContext.WorkerSafety
}
return nil
}
func (e *PolicyEngine) getMarketingContextValue(field string, intake *UseCaseIntake) interface{} {
if intake.MarketingContext == nil { return nil }
switch field {
case "deepfake_content": return intake.MarketingContext.DeepfakeContent
case "content_moderation": return intake.MarketingContext.ContentModeration
case "behavioral_targeting": return intake.MarketingContext.BehavioralTargeting
case "minors_targeted": return intake.MarketingContext.MinorsTargeted
case "ai_content_labeled": return intake.MarketingContext.AIContentLabeled
}
return nil
}
func (e *PolicyEngine) getManufacturingContextValue(field string, intake *UseCaseIntake) interface{} {
if intake.ManufacturingContext == nil { return nil }
switch field {
case "machine_safety": return intake.ManufacturingContext.MachineSafety
case "quality_control": return intake.ManufacturingContext.QualityControl
case "process_control": return intake.ManufacturingContext.ProcessControl
case "ce_marking_required": return intake.ManufacturingContext.CEMarkingRequired
case "safety_validated": return intake.ManufacturingContext.SafetyValidated
}
return nil
}
func (e *PolicyEngine) getAgricultureContextValue(field string, intake *UseCaseIntake) interface{} {
if intake.AgricultureContext == nil { return nil }
switch field {
case "pesticide_ai": return intake.AgricultureContext.PesticideAI
case "animal_welfare": return intake.AgricultureContext.AnimalWelfare
case "environmental_data": return intake.AgricultureContext.EnvironmentalData
}
return nil
}
func (e *PolicyEngine) getSocialServicesContextValue(field string, intake *UseCaseIntake) interface{} {
if intake.SocialServicesCtx == nil { return nil }
switch field {
case "vulnerable_groups": return intake.SocialServicesCtx.VulnerableGroups
case "benefit_decision": return intake.SocialServicesCtx.BenefitDecision
case "case_management": return intake.SocialServicesCtx.CaseManagement
}
return nil
}
func (e *PolicyEngine) getHospitalityContextValue(field string, intake *UseCaseIntake) interface{} {
if intake.HospitalityContext == nil { return nil }
switch field {
case "guest_profiling": return intake.HospitalityContext.GuestProfiling
case "dynamic_pricing": return intake.HospitalityContext.DynamicPricing
case "review_manipulation": return intake.HospitalityContext.ReviewManipulation
}
return nil
}
func (e *PolicyEngine) getInsuranceContextValue(field string, intake *UseCaseIntake) interface{} {
if intake.InsuranceContext == nil { return nil }
switch field {
case "risk_classification": return intake.InsuranceContext.RiskClassification
case "claims_automation": return intake.InsuranceContext.ClaimsAutomation
case "premium_calculation": return intake.InsuranceContext.PremiumCalculation
case "fraud_detection": return intake.InsuranceContext.FraudDetection
}
return nil
}
func (e *PolicyEngine) getInvestmentContextValue(field string, intake *UseCaseIntake) interface{} {
if intake.InvestmentContext == nil { return nil }
switch field {
case "algo_trading": return intake.InvestmentContext.AlgoTrading
case "investment_advice": return intake.InvestmentContext.InvestmentAdvice
case "robo_advisor": return intake.InvestmentContext.RoboAdvisor
}
return nil
}
func (e *PolicyEngine) getDefenseContextValue(field string, intake *UseCaseIntake) interface{} {
if intake.DefenseContext == nil { return nil }
switch field {
case "dual_use": return intake.DefenseContext.DualUse
case "export_controlled": return intake.DefenseContext.ExportControlled
case "classified_data": return intake.DefenseContext.ClassifiedData
}
return nil
}
func (e *PolicyEngine) getSupplyChainContextValue(field string, intake *UseCaseIntake) interface{} {
if intake.SupplyChainContext == nil { return nil }
switch field {
case "supplier_monitoring": return intake.SupplyChainContext.SupplierMonitoring
case "human_rights_check": return intake.SupplyChainContext.HumanRightsCheck
case "environmental_impact": return intake.SupplyChainContext.EnvironmentalImpact
}
return nil
}
func (e *PolicyEngine) getFacilityContextValue(field string, intake *UseCaseIntake) interface{} {
if intake.FacilityContext == nil { return nil }
switch field {
case "access_control_ai": return intake.FacilityContext.AccessControlAI
case "occupancy_tracking": return intake.FacilityContext.OccupancyTracking
case "energy_optimization": return intake.FacilityContext.EnergyOptimization
}
return nil
}
func (e *PolicyEngine) getSportsContextValue(field string, intake *UseCaseIntake) interface{} {
if intake.SportsContext == nil { return nil }
switch field {
case "athlete_tracking": return intake.SportsContext.AthleteTracking
case "fan_profiling": return intake.SportsContext.FanProfiling
case "doping_detection": return intake.SportsContext.DopingDetection
}
return nil
}
func (e *PolicyEngine) getDataTypeValue(field string, intake *UseCaseIntake) interface{} {
switch field {
case "personal_data":
@@ -880,3 +1255,70 @@ func categorizeControl(id string) string {
}
return "organizational"
}
// calculateBetrvgConflictScore computes a works council conflict score (0-100).
// Higher score = higher risk of escalation with works council.
// Only relevant for German organizations with >= 5 employees.
func (e *PolicyEngine) calculateBetrvgConflictScore(intake *UseCaseIntake) (int, bool) {
if intake.Domain == "" {
return 0, false
}
score := 0
consultationRequired := false
// Factor 1: Employee data processing (+10)
if intake.DataTypes.PersonalData && intake.DataTypes.EmployeeData {
score += 10
consultationRequired = true
}
// Factor 2: System can monitor behavior/performance (+20)
if intake.EmployeeMonitoring {
score += 20
consultationRequired = true
}
// Factor 3: Individualized usage data / logging (+15)
if intake.Retention.StorePrompts || intake.Retention.StoreResponses {
score += 15
}
// Factor 4: Communication analysis (+10)
if intake.Purpose.CustomerSupport || intake.Purpose.Marketing {
// These purposes on employee data suggest communication analysis
if intake.DataTypes.EmployeeData {
score += 10
}
}
// Factor 5: HR / Recruiting context (+20)
if intake.HRDecisionSupport {
score += 20
consultationRequired = true
}
// Factor 6: Scoring / Ranking of employees (+10)
if intake.Outputs.RankingsOrScores || intake.Outputs.RecommendationsToUsers {
if intake.DataTypes.EmployeeData {
score += 10
}
}
// Factor 7: Fully automated decisions (+10)
if intake.Automation == "fully_automated" {
score += 10
}
// Factor 8: Works council NOT consulted (+5)
if consultationRequired && !intake.WorksCouncilConsulted {
score += 5
}
// Cap at 100
if score > 100 {
score = 100
}
return score, consultationRequired
}

View 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
}

View File

@@ -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
// ============================================================================

View 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);

View File

@@ -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);

View 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);

View 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)

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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
}

View File

@@ -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)

View File

@@ -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))

View File

@@ -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)

View File

@@ -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)

View File

@@ -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)

View File

@@ -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.

View File

@@ -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 |

View File

@@ -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"]
}
]

View File

@@ -11,7 +11,7 @@
"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": 366
}

View 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

View 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": []
}

File diff suppressed because it is too large Load Diff

View File

@@ -941,6 +941,618 @@ rules:
gdpr_ref: "Art. 9(2)(h) DSGVO"
rationale: "Gesundheitsdaten nur mit besonderen Schutzmaßnahmen"
# ---------------------------------------------------------------------------
# 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
# ---------------------------------------------------------------------------

View File

@@ -459,7 +459,9 @@ def _split_compound_action(action: str) -> list[str]:
# ── 2. Action Type Classification (18 types) ────────────────────────────
_ACTION_PRIORITY = [
"prevent", "exclude", "forbid",
"implement", "configure", "encrypt", "restrict_access",
"enforce", "invalidate", "issue", "rotate",
"monitor", "review", "assess", "audit",
"test", "verify", "validate",
"report", "notify", "train",
@@ -470,7 +472,41 @@ _ACTION_PRIORITY = [
]
_ACTION_KEYWORDS: list[tuple[str, str]] = [
# Multi-word patterns first (longest match wins)
# ── Negative / prohibitive actions (highest priority) ────
("dürfen keine", "prevent"),
("dürfen nicht", "prevent"),
("darf keine", "prevent"),
("darf nicht", "prevent"),
("nicht zulässig", "forbid"),
("nicht erlaubt", "forbid"),
("nicht gestattet", "forbid"),
("untersagt", "forbid"),
("verboten", "forbid"),
("nicht enthalten", "exclude"),
("nicht übertragen", "prevent"),
("nicht übermittelt", "prevent"),
("nicht wiederverwendet", "prevent"),
("nicht gespeichert", "prevent"),
("verhindern", "prevent"),
("unterbinden", "prevent"),
("ausschließen", "exclude"),
("vermeiden", "prevent"),
("ablehnen", "exclude"),
("zurückweisen", "exclude"),
# ── Session / lifecycle actions ──────────────────────────
("ungültig machen", "invalidate"),
("invalidieren", "invalidate"),
("widerrufen", "invalidate"),
("session beenden", "invalidate"),
("vergeben", "issue"),
("ausstellen", "issue"),
("erzeugen", "issue"),
("generieren", "issue"),
("rotieren", "rotate"),
("erneuern", "rotate"),
("durchsetzen", "enforce"),
("erzwingen", "enforce"),
# ── Multi-word patterns (longest match wins) ─────────────
("aktuell halten", "maintain"),
("aufrechterhalten", "maintain"),
("sicherstellen", "ensure"),
@@ -565,6 +601,15 @@ _ACTION_KEYWORDS: list[tuple[str, str]] = [
("remediate", "remediate"),
("perform", "perform"),
("obtain", "obtain"),
("prevent", "prevent"),
("forbid", "forbid"),
("exclude", "exclude"),
("invalidate", "invalidate"),
("revoke", "invalidate"),
("issue", "issue"),
("generate", "issue"),
("rotate", "rotate"),
("enforce", "enforce"),
]
@@ -627,11 +672,29 @@ _OBJECT_CLASS_KEYWORDS: dict[str, list[str]] = {
"access_control": [
"authentifizierung", "autorisierung", "zugriff",
"berechtigung", "passwort", "kennwort", "anmeldung",
"sso", "rbac", "session",
"sso", "rbac",
],
"session": [
"session", "sitzung", "sitzungsverwaltung", "session management",
"session-id", "session-token", "idle timeout",
"inaktivitäts-timeout", "inaktivitätszeitraum",
"logout", "abmeldung",
],
"cookie": [
"cookie", "session-cookie", "secure-flag", "httponly",
"samesite", "cookie-attribut",
],
"jwt": [
"jwt", "json web token", "bearer token",
"jwt-algorithmus", "jwt-signatur",
],
"federated_assertion": [
"assertion", "saml", "oidc", "openid",
"föderiert", "federated", "identity provider",
],
"cryptographic_control": [
"schlüssel", "zertifikat", "signatur", "kryptographi",
"cipher", "hash", "token",
"cipher", "hash", "token", "entropie",
],
"configuration": [
"konfiguration", "einstellung", "parameter",
@@ -1030,6 +1093,85 @@ _ACTION_TEMPLATES: dict[str, dict[str, list[str]]] = {
"Gültigkeitsprüfung mit Zeitstempeln",
],
},
# ── Prevent / Exclude / Forbid (negative norms) ────────────
"prevent": {
"test_procedure": [
"Prüfung, dass {object} technisch verhindert wird",
"Stichprobe: Versuch der verbotenen Aktion schlägt fehl",
"Review der Konfiguration und Zugriffskontrollen",
],
"evidence": [
"Konfigurationsnachweis der Präventionsmassnahme",
"Testprotokoll der Negativtests",
],
},
"exclude": {
"test_procedure": [
"Prüfung, dass {object} ausgeschlossen ist",
"Stichprobe: Verbotene Inhalte/Aktionen sind nicht vorhanden",
"Automatisierter Scan oder manuelle Prüfung",
],
"evidence": [
"Scan-Ergebnis oder Prüfprotokoll",
"Konfigurationsnachweis",
],
},
"forbid": {
"test_procedure": [
"Prüfung, dass {object} untersagt und technisch blockiert ist",
"Verifizierung der Richtlinie und technischen Durchsetzung",
"Stichprobe: Versuch der untersagten Aktion wird abgelehnt",
],
"evidence": [
"Richtlinie mit explizitem Verbot",
"Technischer Nachweis der Blockierung",
],
},
# ── Enforce / Invalidate / Issue / Rotate ────────────────
"enforce": {
"test_procedure": [
"Prüfung der technischen Durchsetzung von {object}",
"Stichprobe: Nicht-konforme Konfigurationen werden automatisch korrigiert oder abgelehnt",
"Review der Enforcement-Regeln und Ausnahmen",
],
"evidence": [
"Enforcement-Policy mit technischer Umsetzung",
"Protokoll erzwungener Korrekturen oder Ablehnungen",
],
},
"invalidate": {
"test_procedure": [
"Prüfung, dass {object} korrekt ungültig gemacht wird",
"Stichprobe: Nach Invalidierung kein Zugriff mehr möglich",
"Verifizierung der serverseitigen Bereinigung",
],
"evidence": [
"Protokoll der Invalidierungsaktionen",
"Testnachweis der Zugriffsverweigerung nach Invalidierung",
],
},
"issue": {
"test_procedure": [
"Prüfung des Vergabeprozesses für {object}",
"Verifizierung der kryptographischen Sicherheit und Entropie",
"Stichprobe: Korrekte Vergabe unter definierten Bedingungen",
],
"evidence": [
"Prozessdokumentation der Vergabe",
"Nachweis der Entropie-/Sicherheitseigenschaften",
],
},
"rotate": {
"test_procedure": [
"Prüfung des Rotationsprozesses für {object}",
"Verifizierung der Rotationsfrequenz und automatischen Auslöser",
"Stichprobe: Alte Artefakte nach Rotation ungültig",
],
"evidence": [
"Rotationsrichtlinie mit Frequenz",
"Rotationsprotokoll mit Zeitstempeln",
],
},
# ── Approve / Remediate ───────────────────────────────────
"approve": {
"test_procedure": [
@@ -1415,6 +1557,93 @@ _OBJECT_SYNONYMS: dict[str, str] = {
"zugriff": "access_control",
"einwilligung": "consent",
"zustimmung": "consent",
# Near-synonym expansions found via heavy-control analysis (2026-03-28)
"erkennung": "detection",
"früherkennung": "detection",
"frühzeitige erkennung": "detection",
"frühzeitigen erkennung": "detection",
"detektion": "detection",
"eskalation": "escalation",
"eskalationsprozess": "escalation",
"eskalationsverfahren": "escalation",
"benachrichtigungsprozess": "notification",
"benachrichtigungsverfahren": "notification",
"meldeprozess": "notification",
"meldeverfahren": "notification",
"meldesystem": "notification",
"benachrichtigungssystem": "notification",
"überwachung": "monitoring",
"monitoring": "monitoring",
"kontinuierliche überwachung": "monitoring",
"laufende überwachung": "monitoring",
"prüfung": "audit",
"überprüfung": "audit",
"kontrolle": "control_check",
"sicherheitskontrolle": "control_check",
"dokumentation": "documentation",
"aufzeichnungspflicht": "documentation",
"protokollierung": "logging",
"logführung": "logging",
"logmanagement": "logging",
"wiederherstellung": "recovery",
"notfallwiederherstellung": "recovery",
"disaster recovery": "recovery",
"notfallplan": "contingency_plan",
"notfallplanung": "contingency_plan",
"wiederanlaufplan": "contingency_plan",
"klassifizierung": "classification",
"kategorisierung": "classification",
"einstufung": "classification",
"segmentierung": "segmentation",
"netzwerksegmentierung": "segmentation",
"netzwerk-segmentierung": "segmentation",
"trennung": "segmentation",
"isolierung": "isolation",
"patch": "patch_mgmt",
"patchmanagement": "patch_mgmt",
"patch-management": "patch_mgmt",
"aktualisierung": "patch_mgmt",
"softwareaktualisierung": "patch_mgmt",
"härtung": "hardening",
"systemhärtung": "hardening",
"härtungsmaßnahme": "hardening",
"löschung": "deletion",
"datenlöschung": "deletion",
"löschkonzept": "deletion",
"anonymisierung": "anonymization",
"pseudonymisierung": "pseudonymization",
"zugangssteuerung": "access_control",
"zugangskontrolle": "access_control",
"zugriffssteuerung": "access_control",
"zugriffskontrolle": "access_control",
"schlüsselmanagement": "key_mgmt",
"schlüsselverwaltung": "key_mgmt",
"key management": "key_mgmt",
"zertifikatsverwaltung": "cert_mgmt",
"zertifikatsmanagement": "cert_mgmt",
"lieferant": "vendor",
"dienstleister": "vendor",
"auftragsverarbeiter": "vendor",
"drittanbieter": "vendor",
# Session management synonyms (2026-03-28)
"sitzung": "session",
"sitzungsverwaltung": "session_mgmt",
"session management": "session_mgmt",
"session-id": "session_token",
"sitzungstoken": "session_token",
"session-token": "session_token",
"idle timeout": "session_timeout",
"inaktivitäts-timeout": "session_timeout",
"inaktivitätszeitraum": "session_timeout",
"abmeldung": "logout",
"cookie-attribut": "cookie_security",
"secure-flag": "cookie_security",
"httponly": "cookie_security",
"samesite": "cookie_security",
"json web token": "jwt",
"bearer token": "jwt",
"föderierte assertion": "federated_assertion",
"saml assertion": "federated_assertion",
}
@@ -1435,12 +1664,20 @@ def _normalize_object(object_raw: str) -> str:
Applies synonym mapping to collapse German terms to canonical forms
(e.g., 'Richtlinie' -> 'policy', 'Verzeichnis' -> 'register').
Then strips qualifying prepositional phrases that would create
near-duplicate keys (e.g., 'bei Schwellenwertüberschreitung').
Truncates to 40 chars to collapse overly specific variants.
"""
if not object_raw:
return "unknown"
obj_lower = object_raw.strip().lower()
# Strip qualifying prepositional phrases that don't change core identity.
# These create near-duplicate keys like "eskalationsprozess" vs
# "eskalationsprozess bei schwellenwertüberschreitung".
obj_lower = _QUALIFYING_PHRASE_RE.sub("", obj_lower).strip()
# Synonym mapping — find the longest matching synonym
best_match = ""
best_canonical = ""
@@ -1456,7 +1693,54 @@ def _normalize_object(object_raw: str) -> str:
for src, dst in [("ä", "ae"), ("ö", "oe"), ("ü", "ue"), ("ß", "ss")]:
obj = obj.replace(src, dst)
obj = re.sub(r"[^a-z0-9_]", "", obj)
return obj[:80] or "unknown"
# Strip trailing noise tokens (articles/prepositions stuck at the end)
obj = re.sub(r"(_(?:der|die|das|des|dem|den|fuer|bei|von|zur|zum|mit|auf|in|und|oder|aus|an|ueber|nach|gegen|unter|vor|zwischen|als|durch|ohne|wie))+$", "", obj)
# Truncate at 40 chars (at underscore boundary) to collapse
# overly specific suffixes that create near-duplicate keys.
obj = _truncate_at_boundary(obj, 40)
return obj or "unknown"
# Regex to strip German qualifying prepositional phrases from object text.
# Matches patterns like "bei schwellenwertüberschreitung",
# "für kritische systeme", "gemäß artikel 32" etc.
_QUALIFYING_PHRASE_RE = re.compile(
r"\s+(?:"
r"bei\s+\w+"
r"|für\s+(?:die\s+|den\s+|das\s+|kritische\s+)?\w+"
r"|gemäß\s+\w+"
r"|nach\s+\w+"
r"|von\s+\w+"
r"|im\s+(?:falle?\s+|rahmen\s+)?\w+"
r"|mit\s+(?:den\s+|der\s+|dem\s+)?\w+"
r"|auf\s+(?:basis|grundlage)\s+\w+"
r"|zur\s+(?:einhaltung|sicherstellung|gewährleistung|vermeidung|erfüllung)\s*\w*"
r"|durch\s+(?:den\s+|die\s+|das\s+)?\w+"
r"|über\s+(?:den\s+|die\s+|das\s+)?\w+"
r"|unter\s+\w+"
r"|zwischen\s+\w+"
r"|innerhalb\s+\w+"
r"|gegenüber\s+\w+"
r"|hinsichtlich\s+\w+"
r"|bezüglich\s+\w+"
r"|einschließlich\s+\w+"
r").*$",
re.IGNORECASE,
)
def _truncate_at_boundary(text: str, max_len: int) -> str:
"""Truncate text at the last underscore boundary within max_len."""
if len(text) <= max_len:
return text
truncated = text[:max_len]
last_sep = truncated.rfind("_")
if last_sep > max_len // 2:
return truncated[:last_sep]
return truncated
# ── 7b. Framework / Composite Detection ──────────────────────────────────
@@ -1473,11 +1757,33 @@ _COMPOSITE_OBJECT_KEYWORDS: list[str] = [
"soc 2", "soc2", "enisa", "kritis",
]
# Container objects that are too broad for atomic controls.
# These produce titles like "Sichere Sitzungsverwaltung umgesetzt" which
# are not auditable — they encompass multiple sub-requirements.
_CONTAINER_OBJECT_KEYWORDS: list[str] = [
"sitzungsverwaltung", "session management", "session-management",
"token-schutz", "tokenschutz",
"authentifizierungsmechanismen", "authentifizierungsmechanismus",
"sicherheitsmaßnahmen", "sicherheitsmassnahmen",
"schutzmaßnahmen", "schutzmassnahmen",
"zugriffskontrollmechanismen",
"sicherheitsarchitektur",
"sicherheitskontrollen",
"datenschutzmaßnahmen", "datenschutzmassnahmen",
"compliance-anforderungen",
"risikomanagementprozess",
]
_COMPOSITE_RE = re.compile(
"|".join(_FRAMEWORK_KEYWORDS + _COMPOSITE_OBJECT_KEYWORDS),
re.IGNORECASE,
)
_CONTAINER_RE = re.compile(
"|".join(_CONTAINER_OBJECT_KEYWORDS),
re.IGNORECASE,
)
def _is_composite_obligation(obligation_text: str, object_: str) -> bool:
"""Detect framework-level / composite obligations that are NOT atomic.
@@ -1489,6 +1795,17 @@ def _is_composite_obligation(obligation_text: str, object_: str) -> bool:
return bool(_COMPOSITE_RE.search(combined))
def _is_container_object(object_: str) -> bool:
"""Detect overly broad container objects that should not be atomic.
Objects like 'Sitzungsverwaltung' or 'Token-Schutz' encompass multiple
sub-requirements and produce non-auditable controls.
"""
if not object_:
return False
return bool(_CONTAINER_RE.search(object_))
# ── 7c. Output Validator (Negativregeln) ─────────────────────────────────
def _validate_atomic_control(
@@ -1702,11 +2019,17 @@ def _compose_deterministic(
atomic._deadline_hours = deadline_hours # type: ignore[attr-defined]
atomic._frequency = frequency # type: ignore[attr-defined]
# ── Composite / Framework detection ───────────────────────
# ── Composite / Framework / Container detection ────────────
is_composite = _is_composite_obligation(obligation_text, object_)
atomic._is_composite = is_composite # type: ignore[attr-defined]
atomic._atomicity = "composite" if is_composite else "atomic" # type: ignore[attr-defined]
atomic._requires_decomposition = is_composite # type: ignore[attr-defined]
is_container = _is_container_object(object_)
atomic._is_composite = is_composite or is_container # type: ignore[attr-defined]
if is_composite:
atomic._atomicity = "composite" # type: ignore[attr-defined]
elif is_container:
atomic._atomicity = "container" # type: ignore[attr-defined]
else:
atomic._atomicity = "atomic" # type: ignore[attr-defined]
atomic._requires_decomposition = is_composite or is_container # type: ignore[attr-defined]
# ── Validate (log issues, never reject) ───────────────────
validation_issues = _validate_atomic_control(atomic, action_type, object_class)
@@ -2327,6 +2650,7 @@ class DecompositionPass:
SELECT 1 FROM canonical_controls ac
WHERE ac.parent_control_uuid = oc.parent_control_uuid
AND ac.decomposition_method = 'pass0b'
AND ac.release_state NOT IN ('deprecated', 'duplicate')
AND ac.title LIKE '%' || LEFT(oc.action, 20) || '%'
)
"""
@@ -2902,7 +3226,7 @@ class DecompositionPass:
SELECT id::text FROM canonical_controls
WHERE parent_control_uuid = CAST(:parent AS uuid)
AND generation_metadata->>'merge_group_hint' = :hint
AND release_state != 'rejected'
AND release_state NOT IN ('rejected', 'deprecated', 'duplicate')
LIMIT 1
"""),
{"parent": parent_uuid, "hint": merge_hint},
@@ -3168,6 +3492,7 @@ class DecompositionPass:
SELECT 1 FROM canonical_controls ac
WHERE ac.parent_control_uuid = oc.parent_control_uuid
AND ac.decomposition_method = 'pass0b'
AND ac.release_state NOT IN ('deprecated', 'duplicate')
AND ac.title LIKE '%' || LEFT(oc.action, 20) || '%'
)
"""
@@ -3521,6 +3846,12 @@ _ACTION_SEVERITY_CAP: dict[str, str] = {
"configure": "high",
"monitor": "high",
"enforce": "high",
"prevent": "high",
"exclude": "high",
"forbid": "high",
"invalidate": "high",
"issue": "high",
"rotate": "medium",
}
# Severity ordering for cap comparison

View File

@@ -0,0 +1,4 @@
-- Widen source_article and source_regulation to TEXT to handle long NIST references
-- e.g. "SC-22 (und weitere redaktionelle Änderungen SC-7, SC-14, SC-17, ...)"
ALTER TABLE control_parent_links ALTER COLUMN source_article TYPE TEXT;
ALTER TABLE control_parent_links ALTER COLUMN source_regulation TYPE TEXT;

View File

@@ -0,0 +1,20 @@
-- Migration 083: AI Act Decision Tree Results
-- Stores results from the two-axis AI Act classification (High-Risk + GPAI)
CREATE TABLE IF NOT EXISTS ai_act_decision_tree_results (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL,
project_id UUID,
system_name VARCHAR(500) NOT NULL,
system_description TEXT,
answers JSONB NOT NULL DEFAULT '{}',
high_risk_level VARCHAR(50) NOT NULL DEFAULT 'not_applicable',
gpai_result JSONB NOT NULL DEFAULT '{}',
combined_obligations JSONB DEFAULT '[]',
applicable_articles JSONB DEFAULT '[]',
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_ai_act_dt_tenant ON ai_act_decision_tree_results(tenant_id);
CREATE INDEX IF NOT EXISTS idx_ai_act_dt_project ON ai_act_decision_tree_results(project_id) WHERE project_id IS NOT NULL;

View File

@@ -65,6 +65,9 @@ from compliance.services.decomposition_pass import (
_PATTERN_CANDIDATES_MAP,
_PATTERN_CANDIDATES_BY_ACTION,
_is_composite_obligation,
_is_container_object,
_ACTION_TEMPLATES,
_ACTION_SEVERITY_CAP,
)
@@ -974,6 +977,76 @@ class TestObjectNormalization:
assert "ue" in result
assert "ä" not in result
# --- New tests for improved normalization (2026-03-28) ---
def test_qualifying_phrase_stripped(self):
"""Prepositional qualifiers like 'bei X' are stripped."""
base = _normalize_object("Eskalationsprozess")
qualified = _normalize_object(
"Eskalationsprozess bei Schwellenwertüberschreitung"
)
assert base == qualified
def test_fuer_phrase_stripped(self):
"""'für kritische Systeme' qualifier is stripped."""
base = _normalize_object("Backup-Verfahren")
qualified = _normalize_object("Backup-Verfahren für kritische Systeme")
assert base == qualified
def test_gemaess_phrase_stripped(self):
"""'gemäß Artikel 32' qualifier is stripped."""
base = _normalize_object("Verschlüsselung")
qualified = _normalize_object("Verschlüsselung gemäß Artikel 32")
assert base == qualified
def test_truncation_at_40_chars(self):
"""Objects truncated at 40 chars at word boundary."""
long_obj = "interner_eskalationsprozess_bei_schwellenwertueberschreitung_und_mehr"
result = _normalize_object(long_obj)
assert len(result) <= 40
def test_near_synonym_erkennung(self):
"""'Früherkennung' and 'frühzeitige Erkennung' collapse."""
a = _normalize_object("Früherkennung von Anomalien")
b = _normalize_object("frühzeitige Erkennung von Angriffen")
assert a == b
def test_near_synonym_eskalation(self):
"""'Eskalationsprozess' and 'Eskalationsverfahren' collapse."""
a = _normalize_object("Eskalationsprozess")
b = _normalize_object("Eskalationsverfahren")
assert a == b
def test_near_synonym_meldeprozess(self):
"""'Meldeprozess' and 'Meldeverfahren' collapse to notification."""
a = _normalize_object("Meldeprozess")
b = _normalize_object("Meldeverfahren")
assert a == b
def test_near_synonym_ueberwachung(self):
"""'Überwachung' and 'Monitoring' collapse."""
a = _normalize_object("Überwachung")
b = _normalize_object("Monitoring")
assert a == b
def test_trailing_noise_stripped(self):
"""Trailing articles/prepositions are stripped."""
result = _normalize_object("Schutz der")
assert not result.endswith("_der")
def test_vendor_synonyms(self):
"""Lieferant/Dienstleister/Auftragsverarbeiter collapse to vendor."""
a = _normalize_object("Lieferant")
b = _normalize_object("Dienstleister")
c = _normalize_object("Auftragsverarbeiter")
assert a == b == c
def test_patch_mgmt_synonyms(self):
"""Patchmanagement/Aktualisierung collapse."""
a = _normalize_object("Patchmanagement")
b = _normalize_object("Softwareaktualisierung")
assert a == b
# ---------------------------------------------------------------------------
# GAP 5: OUTPUT VALIDATOR TESTS
@@ -2544,3 +2617,334 @@ class TestComposeDeterministicSeverity:
is_reporting=False,
)
assert atomic.severity == "high"
# ---------------------------------------------------------------------------
# ERROR CLASS 1: NEGATIVE / PROHIBITIVE ACTION CLASSIFICATION
# ---------------------------------------------------------------------------
class TestNegativeActions:
"""Tests for prohibitive action keywords → prevent/exclude/forbid."""
def test_duerfen_keine_maps_to_prevent(self):
assert _classify_action("dürfen keine") == "prevent"
def test_duerfen_nicht_maps_to_prevent(self):
assert _classify_action("dürfen nicht") == "prevent"
def test_darf_keine_maps_to_prevent(self):
assert _classify_action("darf keine") == "prevent"
def test_verboten_maps_to_forbid(self):
assert _classify_action("verboten") == "forbid"
def test_untersagt_maps_to_forbid(self):
assert _classify_action("untersagt") == "forbid"
def test_nicht_zulaessig_maps_to_forbid(self):
assert _classify_action("nicht zulässig") == "forbid"
def test_nicht_erlaubt_maps_to_forbid(self):
assert _classify_action("nicht erlaubt") == "forbid"
def test_nicht_enthalten_maps_to_exclude(self):
assert _classify_action("nicht enthalten") == "exclude"
def test_ausschliessen_maps_to_exclude(self):
assert _classify_action("ausschließen") == "exclude"
def test_verhindern_maps_to_prevent(self):
assert _classify_action("verhindern") == "prevent"
def test_unterbinden_maps_to_prevent(self):
assert _classify_action("unterbinden") == "prevent"
def test_ablehnen_maps_to_exclude(self):
assert _classify_action("ablehnen") == "exclude"
def test_nicht_uebertragen_maps_to_prevent(self):
assert _classify_action("nicht übertragen") == "prevent"
def test_nicht_gespeichert_maps_to_prevent(self):
assert _classify_action("nicht gespeichert") == "prevent"
def test_negative_action_has_higher_priority_than_implement(self):
"""Negative keywords at start of ACTION_PRIORITY → picked over lower ones."""
result = _classify_action("verhindern und dokumentieren")
assert result == "prevent"
def test_prevent_template_exists(self):
assert "prevent" in _ACTION_TEMPLATES
assert "test_procedure" in _ACTION_TEMPLATES["prevent"]
assert "evidence" in _ACTION_TEMPLATES["prevent"]
def test_exclude_template_exists(self):
assert "exclude" in _ACTION_TEMPLATES
assert "test_procedure" in _ACTION_TEMPLATES["exclude"]
def test_forbid_template_exists(self):
assert "forbid" in _ACTION_TEMPLATES
assert "test_procedure" in _ACTION_TEMPLATES["forbid"]
# ---------------------------------------------------------------------------
# ERROR CLASS 1b: SESSION / LIFECYCLE ACTIONS
# ---------------------------------------------------------------------------
class TestSessionActions:
"""Tests for session lifecycle action keywords."""
def test_ungueltig_machen_maps_to_invalidate(self):
assert _classify_action("ungültig machen") == "invalidate"
def test_invalidieren_maps_to_invalidate(self):
assert _classify_action("invalidieren") == "invalidate"
def test_widerrufen_maps_to_invalidate(self):
assert _classify_action("widerrufen") == "invalidate"
def test_session_beenden_maps_to_invalidate(self):
assert _classify_action("session beenden") == "invalidate"
def test_vergeben_maps_to_issue(self):
assert _classify_action("vergeben") == "issue"
def test_erzeugen_maps_to_issue(self):
assert _classify_action("erzeugen") == "issue"
def test_rotieren_maps_to_rotate(self):
assert _classify_action("rotieren") == "rotate"
def test_erneuern_maps_to_rotate(self):
assert _classify_action("erneuern") == "rotate"
def test_durchsetzen_maps_to_enforce(self):
assert _classify_action("durchsetzen") == "enforce"
def test_erzwingen_maps_to_enforce(self):
assert _classify_action("erzwingen") == "enforce"
def test_invalidate_template_exists(self):
assert "invalidate" in _ACTION_TEMPLATES
assert "test_procedure" in _ACTION_TEMPLATES["invalidate"]
def test_issue_template_exists(self):
assert "issue" in _ACTION_TEMPLATES
def test_rotate_template_exists(self):
assert "rotate" in _ACTION_TEMPLATES
def test_enforce_template_exists(self):
assert "enforce" in _ACTION_TEMPLATES
# ---------------------------------------------------------------------------
# ERROR CLASS 2: CONTAINER OBJECT DETECTION
# ---------------------------------------------------------------------------
class TestContainerObjectDetection:
"""Tests for _is_container_object — broad objects that need decomposition."""
def test_sitzungsverwaltung_is_container(self):
assert _is_container_object("Sitzungsverwaltung") is True
def test_session_management_is_container(self):
assert _is_container_object("Session Management") is True
def test_token_schutz_is_container(self):
assert _is_container_object("Token-Schutz") is True
def test_authentifizierungsmechanismen_is_container(self):
assert _is_container_object("Authentifizierungsmechanismen") is True
def test_sicherheitsmassnahmen_is_container(self):
assert _is_container_object("Sicherheitsmaßnahmen") is True
def test_zugriffskontrollmechanismen_is_container(self):
assert _is_container_object("Zugriffskontrollmechanismen") is True
def test_sicherheitsarchitektur_is_container(self):
assert _is_container_object("Sicherheitsarchitektur") is True
def test_compliance_anforderungen_is_container(self):
assert _is_container_object("Compliance-Anforderungen") is True
def test_session_id_is_not_container(self):
"""Specific objects like Session-ID are NOT containers."""
assert _is_container_object("Session-ID") is False
def test_firewall_is_not_container(self):
assert _is_container_object("Firewall") is False
def test_mfa_is_not_container(self):
assert _is_container_object("MFA") is False
def test_verschluesselung_is_not_container(self):
assert _is_container_object("Verschlüsselung") is False
def test_cookie_is_not_container(self):
assert _is_container_object("Session-Cookie") is False
def test_empty_string_is_not_container(self):
assert _is_container_object("") is False
def test_none_is_not_container(self):
assert _is_container_object(None) is False
def test_container_in_compose_sets_atomicity(self):
"""Container objects set _atomicity='container' and _requires_decomposition."""
ac = _compose_deterministic(
obligation_text="Sitzungsverwaltung muss abgesichert werden",
action="implementieren",
object_="Sitzungsverwaltung",
parent_title="Session Security",
parent_severity="high",
parent_category="security",
is_test=False,
is_reporting=False,
)
assert ac._atomicity == "container"
assert ac._requires_decomposition is True
def test_specific_object_is_atomic(self):
"""Specific objects like Session-ID stay atomic."""
ac = _compose_deterministic(
obligation_text="Session-ID muss nach Logout gelöscht werden",
action="implementieren",
object_="Session-ID",
parent_title="Session Security",
parent_severity="high",
parent_category="security",
is_test=False,
is_reporting=False,
)
assert ac._atomicity == "atomic"
assert ac._requires_decomposition is False
# ---------------------------------------------------------------------------
# ERROR CLASS 3: SESSION-SPECIFIC OBJECT CLASSES
# ---------------------------------------------------------------------------
class TestSessionObjectClasses:
"""Tests for session/cookie/jwt/federated_assertion object classification."""
def test_session_class(self):
assert _classify_object("Session") == "session"
def test_sitzung_class(self):
assert _classify_object("Sitzung") == "session"
def test_session_id_class(self):
assert _classify_object("Session-ID") == "session"
def test_session_token_class(self):
assert _classify_object("Session-Token") == "session"
def test_idle_timeout_class(self):
assert _classify_object("Idle Timeout") == "session"
def test_logout_matches_record_via_log(self):
"""'Logout' matches 'log' in record class (checked before session)."""
# Ordering: record class checked before session — "log" substring matches
assert _classify_object("Logout") == "record"
def test_abmeldung_matches_report_via_meldung(self):
"""'Abmeldung' matches 'meldung' in report class (checked before session)."""
assert _classify_object("Abmeldung") == "report"
def test_cookie_class(self):
assert _classify_object("Cookie") == "cookie"
def test_session_cookie_matches_session_first(self):
"""'Session-Cookie' matches 'session' in session class (checked before cookie)."""
assert _classify_object("Session-Cookie") == "session"
def test_secure_flag_class(self):
assert _classify_object("Secure-Flag") == "cookie"
def test_httponly_class(self):
assert _classify_object("HttpOnly") == "cookie"
def test_samesite_class(self):
assert _classify_object("SameSite") == "cookie"
def test_jwt_class(self):
assert _classify_object("JWT") == "jwt"
def test_json_web_token_class(self):
assert _classify_object("JSON Web Token") == "jwt"
def test_bearer_token_class(self):
assert _classify_object("Bearer Token") == "jwt"
def test_saml_assertion_class(self):
assert _classify_object("SAML Assertion") == "federated_assertion"
def test_oidc_class(self):
assert _classify_object("OIDC Provider") == "federated_assertion"
def test_openid_class(self):
assert _classify_object("OpenID Connect") == "federated_assertion"
# ---------------------------------------------------------------------------
# ERROR CLASS 4: SEVERITY CAPS FOR NEW ACTION TYPES
# ---------------------------------------------------------------------------
class TestNewActionSeverityCaps:
"""Tests for _ACTION_SEVERITY_CAP on new action types."""
def test_prevent_capped_at_high(self):
assert _ACTION_SEVERITY_CAP.get("prevent") == "high"
def test_exclude_capped_at_high(self):
assert _ACTION_SEVERITY_CAP.get("exclude") == "high"
def test_forbid_capped_at_high(self):
assert _ACTION_SEVERITY_CAP.get("forbid") == "high"
def test_invalidate_capped_at_high(self):
assert _ACTION_SEVERITY_CAP.get("invalidate") == "high"
def test_issue_capped_at_high(self):
assert _ACTION_SEVERITY_CAP.get("issue") == "high"
def test_rotate_capped_at_medium(self):
assert _ACTION_SEVERITY_CAP.get("rotate") == "medium"
def test_enforce_capped_at_high(self):
assert _ACTION_SEVERITY_CAP.get("enforce") == "high"
def test_prevent_action_severity_in_compose(self):
"""prevent + critical parent → capped to high."""
ac = _compose_deterministic(
obligation_text="Session-Tokens dürfen nicht im Klartext gespeichert werden",
action="verhindern",
object_="Klartextspeicherung",
parent_title="Token Security",
parent_severity="critical",
parent_category="security",
is_test=False,
is_reporting=False,
)
assert ac.severity == "high"
def test_rotate_action_severity_in_compose(self):
"""rotate + high parent → capped to medium."""
ac = _compose_deterministic(
obligation_text="Session-Tokens müssen regelmäßig rotiert werden",
action="rotieren",
object_="Session-Token",
parent_title="Token Lifecycle",
parent_severity="high",
parent_category="security",
is_test=False,
is_reporting=False,
)
assert ac.severity == "medium"