Files
breakpilot-compliance/admin-compliance/app/api/sdk/drafting-engine/validate/route.ts
Benjamin Admin e6201d5239 feat: Anti-Fake-Evidence System (Phase 1-4b)
Implement full evidence integrity pipeline to prevent compliance theater:
- Confidence levels (E0-E4), truth status tracking, assertion engine
- Four-Eyes approval workflow, audit trail, reject endpoint
- Evidence distribution dashboard, LLM audit routes
- Traceability matrix (backend endpoint + Compliance Hub UI tab)
- Anti-fake badges, control status machine, normative patterns
- 2 migrations, 4 test suites, MkDocs documentation

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-23 17:15:45 +01:00

328 lines
13 KiB
TypeScript

/**
* Drafting Engine - Validate API
*
* Stufe 1: Deterministische Pruefung gegen DOCUMENT_SCOPE_MATRIX
* Stufe 2: LLM Cross-Consistency Check
*/
import { NextRequest, NextResponse } from 'next/server'
import { DOCUMENT_SCOPE_MATRIX, DOCUMENT_TYPE_LABELS, getDepthLevelNumeric } from '@/lib/sdk/compliance-scope-types'
import type { ScopeDocumentType, ComplianceDepthLevel } from '@/lib/sdk/compliance-scope-types'
import type { ValidationContext, ValidationResult, ValidationFinding } from '@/lib/sdk/drafting-engine/types'
import { buildCrossCheckPrompt } from '@/lib/sdk/drafting-engine/prompts/validate-cross-check'
const OLLAMA_URL = process.env.OLLAMA_URL || 'http://host.docker.internal:11434'
const LLM_MODEL = process.env.COMPLIANCE_LLM_MODEL || 'qwen2.5vl:32b'
/**
* Anti-Fake-Evidence: Verbotene Formulierungen
*
* Flags formulations that falsely claim compliance without evidence.
* Only allowed when: control_status=pass AND confidence >= E2 AND
* truth_status in (validated_internal, accepted_by_auditor).
*/
interface EvidenceContext {
controlStatus?: string
confidenceLevel?: string
truthStatus?: string
}
const FORBIDDEN_PATTERNS: Array<{
pattern: RegExp
label: string
safeAlternative: string
}> = [
{ pattern: /ist\s+compliant/gi, label: 'ist compliant', safeAlternative: 'soll compliant sein' },
{ pattern: /erfüllt\s+vollständig/gi, label: 'erfüllt vollständig', safeAlternative: 'soll vollständig erfüllt werden' },
{ pattern: /wurde\s+geprüft/gi, label: 'wurde geprüft', safeAlternative: 'soll geprüft werden' },
{ pattern: /wurde\s+umgesetzt/gi, label: 'wurde umgesetzt', safeAlternative: 'ist zur Umsetzung vorgesehen' },
{ pattern: /ist\s+auditiert/gi, label: 'ist auditiert', safeAlternative: 'soll auditiert werden' },
{ pattern: /vollständig\s+implementiert/gi, label: 'vollständig implementiert', safeAlternative: 'Implementierung ist vorgesehen' },
{ pattern: /nachweislich\s+konform/gi, label: 'nachweislich konform', safeAlternative: 'Konformität ist nachzuweisen' },
]
const CONFIDENCE_ORDER: Record<string, number> = { E0: 0, E1: 1, E2: 2, E3: 3, E4: 4 }
const VALID_TRUTH_STATUSES = new Set(['validated_internal', 'accepted_by_auditor'])
function checkForbiddenFormulations(
content: string,
evidenceContext?: EvidenceContext,
): ValidationFinding[] {
const findings: ValidationFinding[] = []
if (!content) return findings
// If evidence context shows sufficient proof, allow the formulations
if (evidenceContext) {
const { controlStatus, confidenceLevel, truthStatus } = evidenceContext
const confLevel = CONFIDENCE_ORDER[confidenceLevel ?? 'E0'] ?? 0
if (
controlStatus === 'pass' &&
confLevel >= CONFIDENCE_ORDER.E2 &&
VALID_TRUTH_STATUSES.has(truthStatus ?? '')
) {
return findings // Formulations are backed by real evidence
}
}
for (const { pattern, label, safeAlternative } of FORBIDDEN_PATTERNS) {
// Reset regex state for global patterns
pattern.lastIndex = 0
if (pattern.test(content)) {
findings.push({
id: `AFE-FORBIDDEN-${label.replace(/\s+/g, '_').toUpperCase()}`,
severity: 'error',
category: 'forbidden_formulation' as ValidationFinding['category'],
title: `Verbotene Formulierung: "${label}"`,
description: `Die Formulierung "${label}" impliziert eine nachgewiesene Compliance, die ohne ausreichenden Nachweis (Evidence >= E2, validiert) nicht verwendet werden darf.`,
documentType: 'vvt' as ScopeDocumentType,
suggestion: `Verwende stattdessen: "${safeAlternative}"`,
})
}
}
return findings
}
/**
* Stufe 1: Deterministische Pruefung
*/
function deterministicCheck(
documentType: ScopeDocumentType,
validationContext: ValidationContext
): ValidationFinding[] {
const findings: ValidationFinding[] = []
const level = validationContext.scopeLevel
const levelNumeric = getDepthLevelNumeric(level)
const req = DOCUMENT_SCOPE_MATRIX[documentType]?.[level]
// Check 1: Ist das Dokument auf diesem Level erforderlich?
if (req && !req.required && levelNumeric < 3) {
findings.push({
id: `DET-OPT-${documentType}`,
severity: 'suggestion',
category: 'scope_violation',
title: `${DOCUMENT_TYPE_LABELS[documentType] ?? documentType} ist optional`,
description: `Auf Level ${level} ist dieses Dokument nicht verpflichtend.`,
documentType,
})
}
// Check 2: VVT vorhanden wenn erforderlich?
const vvtReq = DOCUMENT_SCOPE_MATRIX.vvt[level]
if (vvtReq.required && validationContext.crossReferences.vvtCategories.length === 0) {
findings.push({
id: 'DET-VVT-MISSING',
severity: 'error',
category: 'missing_content',
title: 'VVT fehlt',
description: `Auf Level ${level} ist ein VVT Pflicht, aber keine Eintraege vorhanden.`,
documentType: 'vvt',
legalReference: 'Art. 30 DSGVO',
})
}
// Check 3: TOM vorhanden wenn VVT existiert?
if (validationContext.crossReferences.vvtCategories.length > 0
&& validationContext.crossReferences.tomControls.length === 0) {
findings.push({
id: 'DET-TOM-MISSING-FOR-VVT',
severity: 'warning',
category: 'cross_reference',
title: 'TOM fehlt bei vorhandenem VVT',
description: 'VVT-Eintraege existieren, aber keine TOM-Massnahmen sind definiert.',
documentType: 'tom',
crossReferenceType: 'vvt',
legalReference: 'Art. 32 DSGVO',
suggestion: 'TOM-Massnahmen erstellen, die die VVT-Taetigkeiten absichern.',
})
}
// Check 4: Loeschfristen fuer VVT-Kategorien
if (validationContext.crossReferences.vvtCategories.length > 0
&& validationContext.crossReferences.retentionCategories.length === 0) {
findings.push({
id: 'DET-LF-MISSING-FOR-VVT',
severity: 'warning',
category: 'cross_reference',
title: 'Loeschfristen fehlen',
description: 'VVT-Eintraege existieren, aber keine Loeschfristen sind definiert.',
documentType: 'lf',
crossReferenceType: 'vvt',
legalReference: 'Art. 17 DSGVO',
suggestion: 'Loeschfristen fuer alle VVT-Datenkategorien definieren.',
})
}
// Check 5: DSFA ohne VVT-Grundlage
if (documentType === 'dsfa' && validationContext.crossReferences.vvtCategories.length === 0) {
findings.push({
id: 'DET-DSFA-NO-VVT',
severity: 'error',
category: 'cross_reference',
title: 'DSFA ohne VVT-Grundlage',
description: 'Eine DSFA setzt ein Verarbeitungsverzeichnis voraus. Ohne VVT fehlt die Uebersicht ueber die betroffenen Verarbeitungstaetigkeiten.',
documentType: 'dsfa',
crossReferenceType: 'vvt',
legalReference: 'Art. 35 i.V.m. Art. 30 DSGVO',
suggestion: 'Zuerst ein VVT erstellen, dann die DSFA darauf aufbauen.',
})
}
// Check 6: DSFA ohne TOM-Massnahmen
if (documentType === 'dsfa' && validationContext.crossReferences.tomControls.length === 0) {
findings.push({
id: 'DET-DSFA-NO-TOM',
severity: 'error',
category: 'cross_reference',
title: 'DSFA ohne TOM-Massnahmen',
description: 'Eine DSFA muss Abhilfemassnahmen enthalten. Ohne TOM-Katalog koennen keine Schutzmassnahmen referenziert werden.',
documentType: 'dsfa',
crossReferenceType: 'tom',
legalReference: 'Art. 35 Abs. 7d DSGVO',
suggestion: 'TOM-Massnahmen definieren, bevor die DSFA erstellt wird.',
})
}
// Check 7: Datenschutzerklaerung ohne Loeschfristen
if (documentType === 'dsi' && validationContext.crossReferences.retentionCategories.length === 0) {
findings.push({
id: 'DET-DSI-NO-LF',
severity: 'warning',
category: 'cross_reference',
title: 'Datenschutzerklaerung ohne Loeschfristen',
description: 'Die Datenschutzerklaerung muss Angaben zur Speicherdauer enthalten. Ohne definierte Loeschfristen fehlt diese Information.',
documentType: 'dsi',
crossReferenceType: 'lf',
legalReference: 'Art. 13 Abs. 2a DSGVO',
suggestion: 'Loeschfristen definieren und in der Datenschutzerklaerung referenzieren.',
})
}
// Check 8: AVV ohne VVT-Kontext
if (documentType === 'av_vertrag' && validationContext.crossReferences.vvtCategories.length === 0) {
findings.push({
id: 'DET-AV-NO-VVT',
severity: 'warning',
category: 'cross_reference',
title: 'AVV ohne VVT-Kontext',
description: 'Ein Auftragsverarbeitungsvertrag sollte auf den im VVT dokumentierten Verarbeitungstaetigkeiten basieren.',
documentType: 'av_vertrag',
crossReferenceType: 'vvt',
legalReference: 'Art. 28 Abs. 3 i.V.m. Art. 30 DSGVO',
suggestion: 'VVT erstellen, um die betroffenen Verarbeitungstaetigkeiten fuer den AVV zu identifizieren.',
})
}
return findings
}
export async function POST(request: NextRequest) {
try {
const body = await request.json()
const { documentType, draftContent, validationContext } = body
if (!documentType || !validationContext) {
return NextResponse.json(
{ error: 'documentType und validationContext sind erforderlich' },
{ status: 400 }
)
}
// ---------------------------------------------------------------
// Stufe 1: Deterministische Pruefung
// ---------------------------------------------------------------
const deterministicFindings = deterministicCheck(documentType, validationContext)
// ---------------------------------------------------------------
// Stufe 2: LLM Cross-Consistency Check
// ---------------------------------------------------------------
let llmFindings: ValidationFinding[] = []
try {
const crossCheckPrompt = buildCrossCheckPrompt({
context: validationContext,
})
const ollamaResponse = await fetch(`${OLLAMA_URL}/api/chat`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
model: LLM_MODEL,
messages: [
{
role: 'system',
content: 'Du bist ein DSGVO-Compliance-Validator. Antworte NUR im JSON-Format.',
},
{ role: 'user', content: crossCheckPrompt },
],
stream: false,
think: false,
options: { temperature: 0.1, num_predict: 8192, num_ctx: 8192 },
format: 'json',
}),
signal: AbortSignal.timeout(120000),
})
if (ollamaResponse.ok) {
const result = await ollamaResponse.json()
try {
const parsed = JSON.parse(result.message?.content || '{}')
llmFindings = [
...(parsed.errors || []),
...(parsed.warnings || []),
...(parsed.suggestions || []),
].map((f: Record<string, unknown>, i: number) => ({
id: String(f.id || `LLM-${i}`),
severity: (String(f.severity || 'suggestion')) as 'error' | 'warning' | 'suggestion',
category: (String(f.category || 'inconsistency')) as ValidationFinding['category'],
title: String(f.title || ''),
description: String(f.description || ''),
documentType: (String(f.documentType || documentType)) as ScopeDocumentType,
crossReferenceType: f.crossReferenceType ? String(f.crossReferenceType) as ScopeDocumentType : undefined,
legalReference: f.legalReference ? String(f.legalReference) : undefined,
suggestion: f.suggestion ? String(f.suggestion) : undefined,
}))
} catch {
// LLM response not parseable, skip
}
}
} catch {
// LLM unavailable, continue with deterministic results only
}
// ---------------------------------------------------------------
// Stufe 1b: Verbotene Formulierungen (Anti-Fake-Evidence)
// ---------------------------------------------------------------
const forbiddenFindings = checkForbiddenFormulations(
draftContent || '',
validationContext.evidenceContext,
)
// ---------------------------------------------------------------
// Combine results
// ---------------------------------------------------------------
const allFindings = [...deterministicFindings, ...forbiddenFindings, ...llmFindings]
const errors = allFindings.filter(f => f.severity === 'error')
const warnings = allFindings.filter(f => f.severity === 'warning')
const suggestions = allFindings.filter(f => f.severity === 'suggestion')
const result: ValidationResult = {
passed: errors.length === 0,
timestamp: new Date().toISOString(),
scopeLevel: validationContext.scopeLevel,
errors,
warnings,
suggestions,
}
return NextResponse.json(result)
} catch (error) {
console.error('Validation error:', error)
return NextResponse.json(
{ error: 'Validierung fehlgeschlagen.' },
{ status: 503 }
)
}
}