Compare commits
4 Commits
7cc420bd9e
...
799668e472
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
799668e472 | ||
|
|
5c7c0055ff | ||
|
|
ec53ba0350 | ||
|
|
34fc8dc654 |
@@ -220,7 +220,7 @@ export const SDK_FLOW_STEPS: SDKFlowStep[] = [
|
||||
inputs: ['modules'],
|
||||
outputs: ['sourcePolicy'],
|
||||
prerequisiteSteps: ['modules'],
|
||||
dbTables: ['compliance_source_policies', 'compliance_allowed_sources', 'compliance_pii_field_rules', 'compliance_source_policy_audit'],
|
||||
dbTables: ['compliance_allowed_sources', 'compliance_pii_rules', 'compliance_source_operations', 'compliance_source_policy_audit'],
|
||||
dbMode: 'read/write',
|
||||
ragCollections: [],
|
||||
isOptional: false,
|
||||
@@ -480,14 +480,14 @@ export const SDK_FLOW_STEPS: SDKFlowStep[] = [
|
||||
checkpointId: 'CP-VVT',
|
||||
checkpointType: 'REQUIRED',
|
||||
checkpointReviewer: 'DSB',
|
||||
description: 'Erstellung des Verzeichnisses aller Verarbeitungstaetigkeiten nach Art. 30 DSGVO.',
|
||||
descriptionLong: 'Das VVT (Verzeichnis von Verarbeitungstaetigkeiten) ist eine gesetzliche Pflichtdokumentation nach Art. 30 DSGVO. Fuer jede Verarbeitungstaetigkeit wird dokumentiert: Zweck, Rechtsgrundlage, Kategorien betroffener Personen, Datenkategorien, Empfaenger, Drittlandtransfers, Loeschfristen und TOMs. Das VVT wird automatisch aus den vorherigen Schritten zusammengestellt (Module, TOMs, Data Mapping). Der DSB muss das VVT freigeben.',
|
||||
description: 'Erstellung des Verzeichnisses aller Verarbeitungstaetigkeiten nach Art. 30 DSGVO — vollstaendig backend-persistent.',
|
||||
descriptionLong: 'Das VVT (Verzeichnis von Verarbeitungstaetigkeiten) ist eine gesetzliche Pflichtdokumentation nach Art. 30 DSGVO. Fuer jede Verarbeitungstaetigkeit wird dokumentiert: Zweck, Rechtsgrundlage, Kategorien betroffener Personen, Datenkategorien, Empfaenger, Drittlandtransfers, Loeschfristen und TOMs. Jede Aktivitaet wird mit einem eindeutigen VVT-ID versehen und in der Datenbank gespeichert (compliance_vvt_activities). Organisationsweite Metadaten (DSB-Kontakt, Branche, Standorte) werden separat verwaltet (compliance_vvt_organization). Alle Aenderungen werden in einem Audit-Log protokolliert (compliance_vvt_audit_log). Das VVT wird via FastAPI-Backend (backend-compliance:8002) persistent gespeichert. Der DSB muss das VVT freigeben.',
|
||||
legalBasis: 'Art. 30 DSGVO (Verzeichnis von Verarbeitungstaetigkeiten)',
|
||||
inputs: ['modules', 'toms', 'dataMapping'],
|
||||
outputs: ['vvt'],
|
||||
prerequisiteSteps: ['loeschfristen'],
|
||||
dbTables: [],
|
||||
dbMode: 'none',
|
||||
dbTables: ['compliance_vvt_organization', 'compliance_vvt_activities', 'compliance_vvt_audit_log'],
|
||||
dbMode: 'read/write',
|
||||
ragCollections: ['bp_compliance_gesetze'],
|
||||
ragPurpose: 'Art. 30 DSGVO Vorlage',
|
||||
isOptional: false,
|
||||
@@ -573,8 +573,8 @@ export const SDK_FLOW_STEPS: SDKFlowStep[] = [
|
||||
checkpointId: 'CP-DOCGEN',
|
||||
checkpointType: 'RECOMMENDED',
|
||||
checkpointReviewer: 'NONE',
|
||||
description: 'Generierung weiterer rechtlicher Dokumente (Impressum, AVV, Auftragsverarbeitung).',
|
||||
descriptionLong: 'Der Dokumentengenerator erstellt zusaetzliche rechtliche Dokumente, die ueber die Pflichtdokumente hinausgehen: Impressum (nach TMG/DDG), Auftragsverarbeitungsvertraege (AVV nach Art. 28 DSGVO), Vertraulichkeitsvereinbarungen, Betriebsvereinbarungen zum Datenschutz und Datenschutz-Folgenabschaetzungs-Berichte. Die Templates werden aus bp_legal_templates geladen und mit den unternehmensspezifischen Daten befuellt.',
|
||||
description: 'Generierung weiterer rechtlicher Dokumente (Impressum, AVV, Auftragsverarbeitung) mit PDF-Export.',
|
||||
descriptionLong: 'Der Dokumentengenerator erstellt zusaetzliche rechtliche Dokumente, die ueber die Pflichtdokumente hinausgehen: Impressum (nach TMG/DDG), Auftragsverarbeitungsvertraege (AVV nach Art. 28 DSGVO), Vertraulichkeitsvereinbarungen, Betriebsvereinbarungen zum Datenschutz und Datenschutz-Folgenabschaetzungs-Berichte. Die Templates werden aus bp_legal_templates geladen und mit den unternehmensspezifischen Daten befuellt. PDF-Export ist direkt im Browser via window.print() moeglich. Steht der Template-Service (breakpilot-core) nicht bereit, erscheint ein Fallback-Banner mit Hinweis.',
|
||||
legalBasis: 'Art. 28 DSGVO (Auftragsverarbeitung), DDG § 5 (Impressum)',
|
||||
inputs: ['companyProfile', 'toms', 'vvt'],
|
||||
outputs: ['generatedDocuments'],
|
||||
@@ -790,13 +790,13 @@ export const SDK_FLOW_STEPS: SDKFlowStep[] = [
|
||||
checkpointId: 'CP-TRAIN',
|
||||
checkpointType: 'REQUIRED',
|
||||
checkpointReviewer: 'NONE',
|
||||
description: 'Durchfuehrung und Tracking von Compliance-Schulungen mit Zertifikaten.',
|
||||
descriptionLong: 'Die Training Engine setzt den Schulungsplan der Academy um. Sie bietet interaktive Schulungsmodule mit Quizzes, Fallbeispielen und Zertifikaten. Jede abgeschlossene Schulung wird dokumentiert (Teilnehmer, Datum, Ergebnis) und dient als Evidence fuer Audits. Die Engine ueberwacht Faelligkeiten, sendet Erinnerungen bei ausstehenden Pflichtschulungen und generiert Compliance-Reports ueber den Schulungsstand aller Mitarbeiter.',
|
||||
description: 'Durchfuehrung und Tracking von Compliance-Schulungen mit Quizzes und Zertifikaten — vollstaendig backend-persistent.',
|
||||
descriptionLong: 'Die Training Engine setzt den Schulungsplan der Academy um. Sie bietet interaktive Schulungsmodule (DSGVO, AI Act, ISO 27001 etc.) mit Quizzes, automatisch generierten Inhalten und Zertifikaten. 28 vordefinierte Schulungsmodule sind hinterlegt. Jede abgeschlossene Schulung wird dokumentiert (Teilnehmer, Datum, Ergebnis, Quiz-Versuch) und dient als Evidence fuer Audits. Die Engine ueberwacht Faelligkeiten, sendet Erinnerungen bei ausstehenden Pflichtschulungen und generiert Compliance-Reports ueber den Schulungsstand aller Mitarbeiter. Backend: ai-compliance-sdk (Go, Port 8093) via /sdk/v1/training/*. Schulungsmatrix ordnet Rollen Pflichtmodulen zu.',
|
||||
inputs: ['trainingPlan', 'modules'],
|
||||
outputs: ['trainingContent'],
|
||||
prerequisiteSteps: ['academy'],
|
||||
dbTables: [],
|
||||
dbMode: 'none',
|
||||
dbTables: ['training_modules', 'training_assignments', 'training_quiz_questions', 'training_quiz_attempts', 'training_matrix_entries', 'training_audit_log'],
|
||||
dbMode: 'read/write',
|
||||
ragCollections: [],
|
||||
isOptional: false,
|
||||
url: '/sdk/training',
|
||||
|
||||
@@ -506,7 +506,7 @@ export default function AuditChecklistPage() {
|
||||
setGeneratingPdf(true)
|
||||
setError(null)
|
||||
try {
|
||||
const res = await fetch(`/api/sdk/v1/compliance/audit/sessions/${activeSessionId}/pdf?language=${pdfLanguage}`)
|
||||
const res = await fetch(`/api/sdk/v1/compliance/audit/sessions/${activeSessionId}/report/pdf?language=${pdfLanguage}`)
|
||||
if (!res.ok) throw new Error('Fehler bei der PDF-Generierung')
|
||||
const blob = await res.blob()
|
||||
const url = window.URL.createObjectURL(blob)
|
||||
|
||||
@@ -104,7 +104,7 @@ export default function AuditReportDetailPage() {
|
||||
setGeneratingPdf(true)
|
||||
setError(null)
|
||||
try {
|
||||
const res = await fetch(`/api/sdk/v1/compliance/audit/sessions/${sessionId}/pdf?language=${pdfLanguage}`)
|
||||
const res = await fetch(`/api/sdk/v1/compliance/audit/sessions/${sessionId}/report/pdf?language=${pdfLanguage}`)
|
||||
if (!res.ok) throw new Error('PDF-Generierung fehlgeschlagen')
|
||||
const blob = await res.blob()
|
||||
const url = window.URL.createObjectURL(blob)
|
||||
|
||||
@@ -133,7 +133,7 @@ export default function AuditReportPage() {
|
||||
const downloadPdf = async (sessionId: string) => {
|
||||
try {
|
||||
setGeneratingPdf(sessionId)
|
||||
const res = await fetch(`/api/sdk/v1/compliance/audit/sessions/${sessionId}/pdf?language=${pdfLanguage}`)
|
||||
const res = await fetch(`/api/sdk/v1/compliance/audit/sessions/${sessionId}/report/pdf?language=${pdfLanguage}`)
|
||||
if (!res.ok) throw new Error('Fehler bei der PDF-Generierung')
|
||||
const blob = await res.blob()
|
||||
const url = window.URL.createObjectURL(blob)
|
||||
|
||||
@@ -454,6 +454,13 @@ export default function DocumentGeneratorPage() {
|
||||
</button>
|
||||
</StepHeader>
|
||||
|
||||
{/* Service unreachable warning */}
|
||||
{!isLoading && !status && (
|
||||
<div className="bg-amber-50 border border-amber-200 rounded-xl p-4 text-sm text-amber-800">
|
||||
<strong>Template-Service nicht erreichbar.</strong> Stellen Sie sicher, dass breakpilot-core läuft (<code>curl -sf http://macmini:8099/health</code>). Das Suchen und Zusammenstellen von Vorlagen ist erst nach Verbindung möglich.
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Status Overview */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||
<div className="bg-white rounded-xl border border-gray-200 p-6">
|
||||
@@ -737,6 +744,24 @@ export default function DocumentGeneratorPage() {
|
||||
Kopieren
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
const printWindow = window.open('', '_blank')
|
||||
if (!printWindow) return
|
||||
let content = combinedContent
|
||||
for (const [key, value] of Object.entries(placeholderValues)) {
|
||||
if (value) {
|
||||
content = content.replace(new RegExp(key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'), value)
|
||||
}
|
||||
}
|
||||
const attributions = selectedTemplateObjects
|
||||
.filter(t => t.attributionRequired && t.attributionText)
|
||||
.map(t => `<li>${t.attributionText}</li>`)
|
||||
.join('')
|
||||
printWindow.document.write(`<!DOCTYPE html><html lang="de"><head><meta charset="UTF-8"><title>Datenschutzdokument</title><style>body{font-family:Arial,sans-serif;max-width:800px;margin:40px auto;padding:0 20px;line-height:1.6;color:#222}h2{border-bottom:1px solid #ccc;padding-bottom:8px;margin-top:32px}hr{border:none;border-top:1px solid #eee;margin:24px 0}.attribution{font-size:12px;color:#666;margin-top:48px;border-top:1px solid #ddd;padding-top:16px}@media print{body{margin:0}}</style></head><body><div style="white-space:pre-wrap">${content.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>')}</div>${attributions ? `<div class="attribution"><strong>Quellenangaben:</strong><ul>${attributions}</ul></div>` : ''}</body></html>`)
|
||||
printWindow.document.close()
|
||||
printWindow.focus()
|
||||
setTimeout(() => printWindow.print(), 500)
|
||||
}}
|
||||
className="px-6 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors"
|
||||
>
|
||||
Als PDF exportieren
|
||||
|
||||
@@ -47,25 +47,149 @@ import type { ProfilingAnswers } from '@/lib/sdk/vvt-profiling'
|
||||
|
||||
type Tab = 'verzeichnis' | 'editor' | 'generator' | 'export'
|
||||
|
||||
const STORAGE_KEY = 'bp_vvt'
|
||||
const PROFILING_STORAGE_KEY = 'bp_vvt_profiling'
|
||||
|
||||
interface VVTData {
|
||||
activities: VVTActivity[]
|
||||
orgHeader: VVTOrganizationHeader
|
||||
profilingAnswers: ProfilingAnswers
|
||||
// =============================================================================
|
||||
// API CLIENT
|
||||
// =============================================================================
|
||||
|
||||
const VVT_API_BASE = '/api/sdk/v1/compliance/vvt'
|
||||
|
||||
function activityFromApi(raw: any): VVTActivity {
|
||||
return {
|
||||
id: raw.id,
|
||||
vvtId: raw.vvt_id,
|
||||
name: raw.name || '',
|
||||
description: raw.description || '',
|
||||
purposes: raw.purposes || [],
|
||||
legalBases: raw.legal_bases || [],
|
||||
dataSubjectCategories: raw.data_subject_categories || [],
|
||||
personalDataCategories: raw.personal_data_categories || [],
|
||||
recipientCategories: raw.recipient_categories || [],
|
||||
thirdCountryTransfers: raw.third_country_transfers || [],
|
||||
retentionPeriod: raw.retention_period || { description: '' },
|
||||
tomDescription: raw.tom_description || '',
|
||||
businessFunction: raw.business_function || 'other',
|
||||
systems: raw.systems || [],
|
||||
deploymentModel: raw.deployment_model || 'cloud',
|
||||
dataSources: raw.data_sources || [],
|
||||
dataFlows: raw.data_flows || [],
|
||||
protectionLevel: raw.protection_level || 'MEDIUM',
|
||||
dpiaRequired: raw.dpia_required || false,
|
||||
structuredToms: raw.structured_toms || { accessControl: [], confidentiality: [], integrity: [], availability: [], separation: [] },
|
||||
status: raw.status || 'DRAFT',
|
||||
responsible: raw.responsible || '',
|
||||
owner: raw.owner || '',
|
||||
createdAt: raw.created_at || new Date().toISOString(),
|
||||
updatedAt: raw.updated_at || raw.created_at || new Date().toISOString(),
|
||||
}
|
||||
}
|
||||
|
||||
function loadData(): VVTData {
|
||||
if (typeof window === 'undefined') return { activities: [], orgHeader: createDefaultOrgHeader(), profilingAnswers: {} }
|
||||
try {
|
||||
const stored = localStorage.getItem(STORAGE_KEY)
|
||||
if (stored) return JSON.parse(stored)
|
||||
} catch { /* ignore */ }
|
||||
return { activities: [], orgHeader: createDefaultOrgHeader(), profilingAnswers: {} }
|
||||
function activityToApi(act: VVTActivity): Record<string, unknown> {
|
||||
return {
|
||||
vvt_id: act.vvtId,
|
||||
name: act.name,
|
||||
description: act.description,
|
||||
purposes: act.purposes,
|
||||
legal_bases: act.legalBases,
|
||||
data_subject_categories: act.dataSubjectCategories,
|
||||
personal_data_categories: act.personalDataCategories,
|
||||
recipient_categories: act.recipientCategories,
|
||||
third_country_transfers: act.thirdCountryTransfers,
|
||||
retention_period: act.retentionPeriod,
|
||||
tom_description: act.tomDescription,
|
||||
business_function: act.businessFunction,
|
||||
systems: act.systems,
|
||||
deployment_model: act.deploymentModel,
|
||||
data_sources: act.dataSources,
|
||||
data_flows: act.dataFlows,
|
||||
protection_level: act.protectionLevel,
|
||||
dpia_required: act.dpiaRequired,
|
||||
structured_toms: act.structuredToms,
|
||||
status: act.status,
|
||||
responsible: act.responsible,
|
||||
owner: act.owner,
|
||||
}
|
||||
}
|
||||
|
||||
function saveData(data: VVTData) {
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(data))
|
||||
function orgHeaderFromApi(raw: any): VVTOrganizationHeader {
|
||||
return {
|
||||
organizationName: raw.organization_name || '',
|
||||
industry: raw.industry || '',
|
||||
locations: raw.locations || [],
|
||||
employeeCount: raw.employee_count || 0,
|
||||
dpoName: raw.dpo_name || '',
|
||||
dpoContact: raw.dpo_contact || '',
|
||||
vvtVersion: raw.vvt_version || '1.0',
|
||||
lastReviewDate: raw.last_review_date || '',
|
||||
nextReviewDate: raw.next_review_date || '',
|
||||
reviewInterval: raw.review_interval || 'annual',
|
||||
}
|
||||
}
|
||||
|
||||
function orgHeaderToApi(org: VVTOrganizationHeader): Record<string, unknown> {
|
||||
return {
|
||||
organization_name: org.organizationName,
|
||||
industry: org.industry,
|
||||
locations: org.locations,
|
||||
employee_count: org.employeeCount,
|
||||
dpo_name: org.dpoName,
|
||||
dpo_contact: org.dpoContact,
|
||||
vvt_version: org.vvtVersion,
|
||||
last_review_date: org.lastReviewDate || null,
|
||||
next_review_date: org.nextReviewDate || null,
|
||||
review_interval: org.reviewInterval,
|
||||
}
|
||||
}
|
||||
|
||||
async function apiListActivities(): Promise<VVTActivity[]> {
|
||||
const res = await fetch(`${VVT_API_BASE}/activities`)
|
||||
if (!res.ok) throw new Error(`GET activities failed: ${res.status}`)
|
||||
const data = await res.json()
|
||||
return data.map(activityFromApi)
|
||||
}
|
||||
|
||||
async function apiGetOrganization(): Promise<VVTOrganizationHeader | null> {
|
||||
const res = await fetch(`${VVT_API_BASE}/organization`)
|
||||
if (!res.ok) return null
|
||||
const data = await res.json()
|
||||
if (!data) return null
|
||||
return orgHeaderFromApi(data)
|
||||
}
|
||||
|
||||
async function apiCreateActivity(act: VVTActivity): Promise<VVTActivity> {
|
||||
const res = await fetch(`${VVT_API_BASE}/activities`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(activityToApi(act)),
|
||||
})
|
||||
if (!res.ok) throw new Error(`POST activity failed: ${res.status}`)
|
||||
return activityFromApi(await res.json())
|
||||
}
|
||||
|
||||
async function apiUpdateActivity(id: string, act: VVTActivity): Promise<VVTActivity> {
|
||||
const res = await fetch(`${VVT_API_BASE}/activities/${id}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(activityToApi(act)),
|
||||
})
|
||||
if (!res.ok) throw new Error(`PUT activity failed: ${res.status}`)
|
||||
return activityFromApi(await res.json())
|
||||
}
|
||||
|
||||
async function apiDeleteActivity(id: string): Promise<void> {
|
||||
const res = await fetch(`${VVT_API_BASE}/activities/${id}`, { method: 'DELETE' })
|
||||
if (!res.ok) throw new Error(`DELETE activity failed: ${res.status}`)
|
||||
}
|
||||
|
||||
async function apiUpsertOrganization(org: VVTOrganizationHeader): Promise<VVTOrganizationHeader> {
|
||||
const res = await fetch(`${VVT_API_BASE}/organization`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(orgHeaderToApi(org)),
|
||||
})
|
||||
if (!res.ok) throw new Error(`PUT organization failed: ${res.status}`)
|
||||
return orgHeaderFromApi(await res.json())
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
@@ -84,34 +208,45 @@ export default function VVTPage() {
|
||||
const [sortBy, setSortBy] = useState<'name' | 'date' | 'status'>('name')
|
||||
const [generatorStep, setGeneratorStep] = useState(1)
|
||||
const [generatorPreview, setGeneratorPreview] = useState<VVTActivity[] | null>(null)
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
const [apiError, setApiError] = useState<string | null>(null)
|
||||
|
||||
// Load from localStorage
|
||||
// Load profiling answers from localStorage (UI state only)
|
||||
useEffect(() => {
|
||||
const data = loadData()
|
||||
setActivities(data.activities)
|
||||
setOrgHeader(data.orgHeader)
|
||||
setProfilingAnswers(data.profilingAnswers)
|
||||
try {
|
||||
const stored = localStorage.getItem(PROFILING_STORAGE_KEY)
|
||||
if (stored) setProfilingAnswers(JSON.parse(stored))
|
||||
} catch { /* ignore */ }
|
||||
}, [])
|
||||
|
||||
// Save to localStorage on change
|
||||
const persist = useCallback((acts: VVTActivity[], org: VVTOrganizationHeader, prof: ProfilingAnswers) => {
|
||||
saveData({ activities: acts, orgHeader: org, profilingAnswers: prof })
|
||||
// Load activities + org header from API
|
||||
useEffect(() => {
|
||||
async function loadFromApi() {
|
||||
setIsLoading(true)
|
||||
setApiError(null)
|
||||
try {
|
||||
const [acts, org] = await Promise.all([
|
||||
apiListActivities(),
|
||||
apiGetOrganization(),
|
||||
])
|
||||
setActivities(acts)
|
||||
if (org) setOrgHeader(org)
|
||||
} catch (err) {
|
||||
setApiError('Fehler beim Laden der VVT-Daten. Bitte Verbindung prüfen.')
|
||||
console.error('VVT API load error:', err)
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
loadFromApi()
|
||||
}, [])
|
||||
|
||||
const updateActivities = useCallback((acts: VVTActivity[]) => {
|
||||
setActivities(acts)
|
||||
persist(acts, orgHeader, profilingAnswers)
|
||||
}, [orgHeader, profilingAnswers, persist])
|
||||
|
||||
const updateOrgHeader = useCallback((org: VVTOrganizationHeader) => {
|
||||
setOrgHeader(org)
|
||||
persist(activities, org, profilingAnswers)
|
||||
}, [activities, profilingAnswers, persist])
|
||||
|
||||
const updateProfilingAnswers = useCallback((prof: ProfilingAnswers) => {
|
||||
setProfilingAnswers(prof)
|
||||
persist(activities, orgHeader, prof)
|
||||
}, [activities, orgHeader, persist])
|
||||
try {
|
||||
localStorage.setItem(PROFILING_STORAGE_KEY, JSON.stringify(prof))
|
||||
} catch { /* ignore */ }
|
||||
}, [])
|
||||
|
||||
// Computed stats
|
||||
const activeCount = activities.filter(a => a.status === 'APPROVED').length
|
||||
@@ -147,6 +282,14 @@ export default function VVTPage() {
|
||||
{ id: 'export', label: 'Export & Compliance' },
|
||||
]
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-64">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-purple-600"></div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<StepHeader
|
||||
@@ -157,6 +300,12 @@ export default function VVTPage() {
|
||||
tips={stepInfo.tips}
|
||||
/>
|
||||
|
||||
{apiError && (
|
||||
<div className="bg-red-50 border border-red-200 rounded-lg p-4 text-sm text-red-700">
|
||||
{apiError}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Tab Navigation */}
|
||||
<div className="flex items-center gap-1 bg-gray-100 p-1 rounded-lg">
|
||||
{tabs.map(t => (
|
||||
@@ -193,14 +342,28 @@ export default function VVTPage() {
|
||||
sortBy={sortBy}
|
||||
setSortBy={setSortBy}
|
||||
onEdit={(id) => { setEditingId(id); setTab('editor') }}
|
||||
onNew={() => {
|
||||
onNew={async () => {
|
||||
const vvtId = generateVVTId(activities.map(a => a.vvtId))
|
||||
const newAct = createEmptyActivity(vvtId)
|
||||
updateActivities([...activities, newAct])
|
||||
setEditingId(newAct.id)
|
||||
setTab('editor')
|
||||
try {
|
||||
const created = await apiCreateActivity(newAct)
|
||||
setActivities(prev => [...prev, created])
|
||||
setEditingId(created.id)
|
||||
setTab('editor')
|
||||
} catch (err) {
|
||||
setApiError('Fehler beim Anlegen der Verarbeitung.')
|
||||
console.error(err)
|
||||
}
|
||||
}}
|
||||
onDelete={async (id) => {
|
||||
try {
|
||||
await apiDeleteActivity(id)
|
||||
setActivities(prev => prev.filter(a => a.id !== id))
|
||||
} catch (err) {
|
||||
setApiError('Fehler beim Löschen der Verarbeitung.')
|
||||
console.error(err)
|
||||
}
|
||||
}}
|
||||
onDelete={(id) => updateActivities(activities.filter(a => a.id !== id))}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -208,12 +371,13 @@ export default function VVTPage() {
|
||||
<TabEditor
|
||||
activity={editingActivity}
|
||||
activities={activities}
|
||||
onSave={(updated) => {
|
||||
const idx = activities.findIndex(a => a.id === updated.id)
|
||||
if (idx >= 0) {
|
||||
const copy = [...activities]
|
||||
copy[idx] = { ...updated, updatedAt: new Date().toISOString() }
|
||||
updateActivities(copy)
|
||||
onSave={async (updated) => {
|
||||
try {
|
||||
const saved = await apiUpdateActivity(updated.id, updated)
|
||||
setActivities(prev => prev.map(a => a.id === saved.id ? saved : a))
|
||||
} catch (err) {
|
||||
setApiError('Fehler beim Speichern der Verarbeitung.')
|
||||
console.error(err)
|
||||
}
|
||||
}}
|
||||
onBack={() => setTab('verzeichnis')}
|
||||
@@ -229,8 +393,17 @@ export default function VVTPage() {
|
||||
setAnswers={updateProfilingAnswers}
|
||||
preview={generatorPreview}
|
||||
setPreview={setGeneratorPreview}
|
||||
onAdoptAll={(newActivities) => {
|
||||
updateActivities([...activities, ...newActivities])
|
||||
onAdoptAll={async (newActivities) => {
|
||||
const created: VVTActivity[] = []
|
||||
for (const act of newActivities) {
|
||||
try {
|
||||
const saved = await apiCreateActivity(act)
|
||||
created.push(saved)
|
||||
} catch (err) {
|
||||
console.error('Failed to create activity from generator:', err)
|
||||
}
|
||||
}
|
||||
if (created.length > 0) setActivities(prev => [...prev, ...created])
|
||||
setGeneratorPreview(null)
|
||||
setGeneratorStep(1)
|
||||
setTab('verzeichnis')
|
||||
@@ -242,7 +415,15 @@ export default function VVTPage() {
|
||||
<TabExport
|
||||
activities={activities}
|
||||
orgHeader={orgHeader}
|
||||
onUpdateOrgHeader={updateOrgHeader}
|
||||
onUpdateOrgHeader={async (org) => {
|
||||
try {
|
||||
const saved = await apiUpsertOrganization(org)
|
||||
setOrgHeader(saved)
|
||||
} catch (err) {
|
||||
setApiError('Fehler beim Speichern der Organisationsdaten.')
|
||||
console.error(err)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -56,6 +56,9 @@ export function PIIRulesTab({ apiBase, onUpdate }: PIIRulesTabProps) {
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
// Category filter
|
||||
const [categoryFilter, setCategoryFilter] = useState('')
|
||||
|
||||
// Test panel
|
||||
const [testText, setTestText] = useState('')
|
||||
const [testResult, setTestResult] = useState<PIITestResult | null>(null)
|
||||
@@ -77,12 +80,14 @@ export function PIIRulesTab({ apiBase, onUpdate }: PIIRulesTabProps) {
|
||||
|
||||
useEffect(() => {
|
||||
fetchRules()
|
||||
}, [])
|
||||
}, [categoryFilter])
|
||||
|
||||
const fetchRules = async () => {
|
||||
try {
|
||||
setLoading(true)
|
||||
const res = await fetch(`${apiBase}/pii-rules`)
|
||||
const params = new URLSearchParams()
|
||||
if (categoryFilter) params.append('category', categoryFilter)
|
||||
const res = await fetch(`${apiBase}/pii-rules?${params}`)
|
||||
if (!res.ok) throw new Error('Fehler beim Laden')
|
||||
|
||||
const data = await res.json()
|
||||
@@ -321,17 +326,29 @@ Meine IBAN ist DE89 3704 0044 0532 0130 00."
|
||||
</div>
|
||||
|
||||
{/* Rules List Header */}
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<div className="flex flex-wrap justify-between items-center gap-3 mb-4">
|
||||
<h3 className="font-semibold text-slate-900">PII-Erkennungsregeln</h3>
|
||||
<button
|
||||
onClick={() => setIsNewRule(true)}
|
||||
className="px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 flex items-center gap-2"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
|
||||
</svg>
|
||||
Neue Regel
|
||||
</button>
|
||||
<div className="flex gap-3 items-center">
|
||||
<select
|
||||
value={categoryFilter}
|
||||
onChange={(e) => setCategoryFilter(e.target.value)}
|
||||
className="px-3 py-2 border border-slate-200 rounded-lg text-sm focus:ring-2 focus:ring-purple-500 focus:border-transparent"
|
||||
>
|
||||
<option value="">Alle Kategorien</option>
|
||||
{CATEGORIES.map((c) => (
|
||||
<option key={c.value} value={c.value}>{c.label}</option>
|
||||
))}
|
||||
</select>
|
||||
<button
|
||||
onClick={() => setIsNewRule(true)}
|
||||
className="px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 flex items-center gap-2"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
|
||||
</svg>
|
||||
Neue Regel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Rules Table */}
|
||||
|
||||
@@ -51,6 +51,7 @@ export function SourcesTab({ apiBase, onUpdate }: SourcesTabProps) {
|
||||
const [searchTerm, setSearchTerm] = useState('')
|
||||
const [licenseFilter, setLicenseFilter] = useState('')
|
||||
const [statusFilter, setStatusFilter] = useState<'all' | 'active' | 'inactive'>('all')
|
||||
const [sourceTypeFilter, setSourceTypeFilter] = useState('')
|
||||
|
||||
// Edit modal
|
||||
const [editingSource, setEditingSource] = useState<AllowedSource | null>(null)
|
||||
@@ -69,7 +70,7 @@ export function SourcesTab({ apiBase, onUpdate }: SourcesTabProps) {
|
||||
|
||||
useEffect(() => {
|
||||
fetchSources()
|
||||
}, [licenseFilter, statusFilter])
|
||||
}, [licenseFilter, statusFilter, sourceTypeFilter])
|
||||
|
||||
const fetchSources = async () => {
|
||||
try {
|
||||
@@ -77,6 +78,7 @@ export function SourcesTab({ apiBase, onUpdate }: SourcesTabProps) {
|
||||
const params = new URLSearchParams()
|
||||
if (licenseFilter) params.append('license', licenseFilter)
|
||||
if (statusFilter !== 'all') params.append('active_only', statusFilter === 'active' ? 'true' : 'false')
|
||||
if (sourceTypeFilter) params.append('source_type', sourceTypeFilter)
|
||||
|
||||
const res = await fetch(`${apiBase}/sources?${params}`)
|
||||
if (!res.ok) throw new Error('Fehler beim Laden')
|
||||
@@ -230,6 +232,18 @@ export function SourcesTab({ apiBase, onUpdate }: SourcesTabProps) {
|
||||
<option value="active">Aktiv</option>
|
||||
<option value="inactive">Inaktiv</option>
|
||||
</select>
|
||||
<select
|
||||
value={sourceTypeFilter}
|
||||
onChange={(e) => setSourceTypeFilter(e.target.value)}
|
||||
className="px-4 py-2 border border-slate-200 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent"
|
||||
>
|
||||
<option value="">Alle Typen</option>
|
||||
<option value="legal">Rechtlich</option>
|
||||
<option value="guidance">Leitlinien</option>
|
||||
<option value="template">Vorlagen</option>
|
||||
<option value="technical">Technisch</option>
|
||||
<option value="other">Sonstige</option>
|
||||
</select>
|
||||
<button
|
||||
onClick={() => setIsNewSource(true)}
|
||||
className="px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 flex items-center gap-2"
|
||||
|
||||
@@ -9,6 +9,7 @@ from .dashboard_routes import router as dashboard_router
|
||||
from .scraper_routes import router as scraper_router
|
||||
from .module_routes import router as module_router
|
||||
from .isms_routes import router as isms_router
|
||||
from .vvt_routes import router as vvt_router
|
||||
|
||||
# Include sub-routers
|
||||
router.include_router(audit_router)
|
||||
@@ -19,6 +20,7 @@ router.include_router(dashboard_router)
|
||||
router.include_router(scraper_router)
|
||||
router.include_router(module_router)
|
||||
router.include_router(isms_router)
|
||||
router.include_router(vvt_router)
|
||||
|
||||
__all__ = [
|
||||
"router",
|
||||
@@ -30,4 +32,5 @@ __all__ = [
|
||||
"scraper_router",
|
||||
"module_router",
|
||||
"isms_router",
|
||||
"vvt_router",
|
||||
]
|
||||
|
||||
@@ -1849,3 +1849,143 @@ class ISO27001OverviewResponse(BaseModel):
|
||||
policies_approved: int
|
||||
objectives_count: int
|
||||
objectives_achieved: int
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# VVT Schemas — Verzeichnis von Verarbeitungstaetigkeiten (Art. 30 DSGVO)
|
||||
# ============================================================================
|
||||
|
||||
class VVTOrganizationUpdate(BaseModel):
|
||||
organization_name: Optional[str] = None
|
||||
industry: Optional[str] = None
|
||||
locations: Optional[List[str]] = None
|
||||
employee_count: Optional[int] = None
|
||||
dpo_name: Optional[str] = None
|
||||
dpo_contact: Optional[str] = None
|
||||
vvt_version: Optional[str] = None
|
||||
last_review_date: Optional[date] = None
|
||||
next_review_date: Optional[date] = None
|
||||
review_interval: Optional[str] = None
|
||||
|
||||
|
||||
class VVTOrganizationResponse(BaseModel):
|
||||
id: str
|
||||
organization_name: str
|
||||
industry: Optional[str] = None
|
||||
locations: List[Any] = []
|
||||
employee_count: Optional[int] = None
|
||||
dpo_name: Optional[str] = None
|
||||
dpo_contact: Optional[str] = None
|
||||
vvt_version: str = '1.0'
|
||||
last_review_date: Optional[date] = None
|
||||
next_review_date: Optional[date] = None
|
||||
review_interval: str = 'annual'
|
||||
created_at: datetime
|
||||
updated_at: Optional[datetime] = None
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class VVTActivityCreate(BaseModel):
|
||||
vvt_id: str
|
||||
name: str
|
||||
description: Optional[str] = None
|
||||
purposes: List[str] = []
|
||||
legal_bases: List[str] = []
|
||||
data_subject_categories: List[str] = []
|
||||
personal_data_categories: List[str] = []
|
||||
recipient_categories: List[str] = []
|
||||
third_country_transfers: List[Any] = []
|
||||
retention_period: Dict[str, Any] = {}
|
||||
tom_description: Optional[str] = None
|
||||
business_function: Optional[str] = None
|
||||
systems: List[str] = []
|
||||
deployment_model: Optional[str] = None
|
||||
data_sources: List[Any] = []
|
||||
data_flows: List[Any] = []
|
||||
protection_level: str = 'MEDIUM'
|
||||
dpia_required: bool = False
|
||||
structured_toms: Dict[str, Any] = {}
|
||||
status: str = 'DRAFT'
|
||||
responsible: Optional[str] = None
|
||||
owner: Optional[str] = None
|
||||
|
||||
|
||||
class VVTActivityUpdate(BaseModel):
|
||||
name: Optional[str] = None
|
||||
description: Optional[str] = None
|
||||
purposes: Optional[List[str]] = None
|
||||
legal_bases: Optional[List[str]] = None
|
||||
data_subject_categories: Optional[List[str]] = None
|
||||
personal_data_categories: Optional[List[str]] = None
|
||||
recipient_categories: Optional[List[str]] = None
|
||||
third_country_transfers: Optional[List[Any]] = None
|
||||
retention_period: Optional[Dict[str, Any]] = None
|
||||
tom_description: Optional[str] = None
|
||||
business_function: Optional[str] = None
|
||||
systems: Optional[List[str]] = None
|
||||
deployment_model: Optional[str] = None
|
||||
data_sources: Optional[List[Any]] = None
|
||||
data_flows: Optional[List[Any]] = None
|
||||
protection_level: Optional[str] = None
|
||||
dpia_required: Optional[bool] = None
|
||||
structured_toms: Optional[Dict[str, Any]] = None
|
||||
status: Optional[str] = None
|
||||
responsible: Optional[str] = None
|
||||
owner: Optional[str] = None
|
||||
|
||||
|
||||
class VVTActivityResponse(BaseModel):
|
||||
id: str
|
||||
vvt_id: str
|
||||
name: str
|
||||
description: Optional[str] = None
|
||||
purposes: List[Any] = []
|
||||
legal_bases: List[Any] = []
|
||||
data_subject_categories: List[Any] = []
|
||||
personal_data_categories: List[Any] = []
|
||||
recipient_categories: List[Any] = []
|
||||
third_country_transfers: List[Any] = []
|
||||
retention_period: Dict[str, Any] = {}
|
||||
tom_description: Optional[str] = None
|
||||
business_function: Optional[str] = None
|
||||
systems: List[Any] = []
|
||||
deployment_model: Optional[str] = None
|
||||
data_sources: List[Any] = []
|
||||
data_flows: List[Any] = []
|
||||
protection_level: str = 'MEDIUM'
|
||||
dpia_required: bool = False
|
||||
structured_toms: Dict[str, Any] = {}
|
||||
status: str = 'DRAFT'
|
||||
responsible: Optional[str] = None
|
||||
owner: Optional[str] = None
|
||||
created_at: datetime
|
||||
updated_at: Optional[datetime] = None
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class VVTStatsResponse(BaseModel):
|
||||
total: int
|
||||
by_status: Dict[str, int]
|
||||
by_business_function: Dict[str, int]
|
||||
dpia_required_count: int
|
||||
third_country_count: int
|
||||
draft_count: int
|
||||
approved_count: int
|
||||
|
||||
|
||||
class VVTAuditLogEntry(BaseModel):
|
||||
id: str
|
||||
action: str
|
||||
entity_type: str
|
||||
entity_id: Optional[str] = None
|
||||
changed_by: Optional[str] = None
|
||||
old_values: Optional[Dict[str, Any]] = None
|
||||
new_values: Optional[Dict[str, Any]] = None
|
||||
created_at: datetime
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
@@ -148,12 +148,18 @@ def _source_to_dict(source: AllowedSourceDB) -> dict:
|
||||
@router.get("/sources")
|
||||
async def list_sources(
|
||||
active_only: bool = Query(False),
|
||||
source_type: Optional[str] = Query(None),
|
||||
license: Optional[str] = Query(None),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""List all allowed sources."""
|
||||
"""List all allowed sources with optional filters."""
|
||||
query = db.query(AllowedSourceDB)
|
||||
if active_only:
|
||||
query = query.filter(AllowedSourceDB.active == True)
|
||||
if source_type:
|
||||
query = query.filter(AllowedSourceDB.source_type == source_type)
|
||||
if license:
|
||||
query = query.filter(AllowedSourceDB.license == license)
|
||||
sources = query.order_by(AllowedSourceDB.name).all()
|
||||
return {
|
||||
"sources": [
|
||||
@@ -328,9 +334,15 @@ async def update_operation(
|
||||
# =============================================================================
|
||||
|
||||
@router.get("/pii-rules")
|
||||
async def list_pii_rules(db: Session = Depends(get_db)):
|
||||
"""List all PII rules."""
|
||||
rules = db.query(PIIRuleDB).order_by(PIIRuleDB.category, PIIRuleDB.name).all()
|
||||
async def list_pii_rules(
|
||||
category: Optional[str] = Query(None),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""List all PII rules with optional category filter."""
|
||||
query = db.query(PIIRuleDB)
|
||||
if category:
|
||||
query = query.filter(PIIRuleDB.category == category)
|
||||
rules = query.order_by(PIIRuleDB.category, PIIRuleDB.name).all()
|
||||
return {
|
||||
"rules": [
|
||||
{
|
||||
|
||||
384
backend-compliance/compliance/api/vvt_routes.py
Normal file
384
backend-compliance/compliance/api/vvt_routes.py
Normal file
@@ -0,0 +1,384 @@
|
||||
"""
|
||||
FastAPI routes for VVT — Verzeichnis von Verarbeitungstaetigkeiten (Art. 30 DSGVO).
|
||||
|
||||
Endpoints:
|
||||
GET /vvt/organization — Load organization header
|
||||
PUT /vvt/organization — Save organization header
|
||||
GET /vvt/activities — List activities (filter: status, business_function)
|
||||
POST /vvt/activities — Create new activity
|
||||
GET /vvt/activities/{id} — Get single activity
|
||||
PUT /vvt/activities/{id} — Update activity
|
||||
DELETE /vvt/activities/{id} — Delete activity
|
||||
GET /vvt/audit-log — Audit trail (limit, offset)
|
||||
GET /vvt/export — JSON export of all activities
|
||||
GET /vvt/stats — Statistics
|
||||
"""
|
||||
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from typing import Optional, List
|
||||
from uuid import uuid4
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from classroom_engine.database import get_db
|
||||
|
||||
from ..db.vvt_models import VVTOrganizationDB, VVTActivityDB, VVTAuditLogDB
|
||||
from .schemas import (
|
||||
VVTOrganizationUpdate, VVTOrganizationResponse,
|
||||
VVTActivityCreate, VVTActivityUpdate, VVTActivityResponse,
|
||||
VVTStatsResponse, VVTAuditLogEntry,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
router = APIRouter(prefix="/vvt", tags=["compliance-vvt"])
|
||||
|
||||
|
||||
def _log_audit(
|
||||
db: Session,
|
||||
action: str,
|
||||
entity_type: str,
|
||||
entity_id=None,
|
||||
changed_by: str = "system",
|
||||
old_values=None,
|
||||
new_values=None,
|
||||
):
|
||||
entry = VVTAuditLogDB(
|
||||
action=action,
|
||||
entity_type=entity_type,
|
||||
entity_id=entity_id,
|
||||
changed_by=changed_by,
|
||||
old_values=old_values,
|
||||
new_values=new_values,
|
||||
)
|
||||
db.add(entry)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Organization Header
|
||||
# ============================================================================
|
||||
|
||||
@router.get("/organization", response_model=Optional[VVTOrganizationResponse])
|
||||
async def get_organization(db: Session = Depends(get_db)):
|
||||
"""Load the VVT organization header (single record)."""
|
||||
org = db.query(VVTOrganizationDB).order_by(VVTOrganizationDB.created_at).first()
|
||||
if not org:
|
||||
return None
|
||||
return VVTOrganizationResponse(
|
||||
id=str(org.id),
|
||||
organization_name=org.organization_name,
|
||||
industry=org.industry,
|
||||
locations=org.locations or [],
|
||||
employee_count=org.employee_count,
|
||||
dpo_name=org.dpo_name,
|
||||
dpo_contact=org.dpo_contact,
|
||||
vvt_version=org.vvt_version or '1.0',
|
||||
last_review_date=org.last_review_date,
|
||||
next_review_date=org.next_review_date,
|
||||
review_interval=org.review_interval or 'annual',
|
||||
created_at=org.created_at,
|
||||
updated_at=org.updated_at,
|
||||
)
|
||||
|
||||
|
||||
@router.put("/organization", response_model=VVTOrganizationResponse)
|
||||
async def upsert_organization(
|
||||
request: VVTOrganizationUpdate,
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Create or update the VVT organization header."""
|
||||
org = db.query(VVTOrganizationDB).order_by(VVTOrganizationDB.created_at).first()
|
||||
|
||||
if not org:
|
||||
data = request.dict(exclude_none=True)
|
||||
if 'organization_name' not in data:
|
||||
data['organization_name'] = 'Meine Organisation'
|
||||
org = VVTOrganizationDB(**data)
|
||||
db.add(org)
|
||||
else:
|
||||
for field, value in request.dict(exclude_none=True).items():
|
||||
setattr(org, field, value)
|
||||
org.updated_at = datetime.utcnow()
|
||||
|
||||
db.commit()
|
||||
db.refresh(org)
|
||||
|
||||
return VVTOrganizationResponse(
|
||||
id=str(org.id),
|
||||
organization_name=org.organization_name,
|
||||
industry=org.industry,
|
||||
locations=org.locations or [],
|
||||
employee_count=org.employee_count,
|
||||
dpo_name=org.dpo_name,
|
||||
dpo_contact=org.dpo_contact,
|
||||
vvt_version=org.vvt_version or '1.0',
|
||||
last_review_date=org.last_review_date,
|
||||
next_review_date=org.next_review_date,
|
||||
review_interval=org.review_interval or 'annual',
|
||||
created_at=org.created_at,
|
||||
updated_at=org.updated_at,
|
||||
)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Activities
|
||||
# ============================================================================
|
||||
|
||||
def _activity_to_response(act: VVTActivityDB) -> VVTActivityResponse:
|
||||
return VVTActivityResponse(
|
||||
id=str(act.id),
|
||||
vvt_id=act.vvt_id,
|
||||
name=act.name,
|
||||
description=act.description,
|
||||
purposes=act.purposes or [],
|
||||
legal_bases=act.legal_bases or [],
|
||||
data_subject_categories=act.data_subject_categories or [],
|
||||
personal_data_categories=act.personal_data_categories or [],
|
||||
recipient_categories=act.recipient_categories or [],
|
||||
third_country_transfers=act.third_country_transfers or [],
|
||||
retention_period=act.retention_period or {},
|
||||
tom_description=act.tom_description,
|
||||
business_function=act.business_function,
|
||||
systems=act.systems or [],
|
||||
deployment_model=act.deployment_model,
|
||||
data_sources=act.data_sources or [],
|
||||
data_flows=act.data_flows or [],
|
||||
protection_level=act.protection_level or 'MEDIUM',
|
||||
dpia_required=act.dpia_required or False,
|
||||
structured_toms=act.structured_toms or {},
|
||||
status=act.status or 'DRAFT',
|
||||
responsible=act.responsible,
|
||||
owner=act.owner,
|
||||
created_at=act.created_at,
|
||||
updated_at=act.updated_at,
|
||||
)
|
||||
|
||||
|
||||
@router.get("/activities", response_model=List[VVTActivityResponse])
|
||||
async def list_activities(
|
||||
status: Optional[str] = Query(None),
|
||||
business_function: Optional[str] = Query(None),
|
||||
search: Optional[str] = Query(None),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""List all processing activities with optional filters."""
|
||||
query = db.query(VVTActivityDB)
|
||||
|
||||
if status:
|
||||
query = query.filter(VVTActivityDB.status == status)
|
||||
if business_function:
|
||||
query = query.filter(VVTActivityDB.business_function == business_function)
|
||||
if search:
|
||||
term = f"%{search}%"
|
||||
query = query.filter(
|
||||
(VVTActivityDB.name.ilike(term)) |
|
||||
(VVTActivityDB.description.ilike(term)) |
|
||||
(VVTActivityDB.vvt_id.ilike(term))
|
||||
)
|
||||
|
||||
activities = query.order_by(VVTActivityDB.created_at.desc()).all()
|
||||
return [_activity_to_response(a) for a in activities]
|
||||
|
||||
|
||||
@router.post("/activities", response_model=VVTActivityResponse, status_code=201)
|
||||
async def create_activity(
|
||||
request: VVTActivityCreate,
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Create a new processing activity."""
|
||||
# Check for duplicate vvt_id
|
||||
existing = db.query(VVTActivityDB).filter(
|
||||
VVTActivityDB.vvt_id == request.vvt_id
|
||||
).first()
|
||||
if existing:
|
||||
raise HTTPException(
|
||||
status_code=409,
|
||||
detail=f"Activity with VVT-ID '{request.vvt_id}' already exists"
|
||||
)
|
||||
|
||||
act = VVTActivityDB(**request.dict())
|
||||
db.add(act)
|
||||
db.flush() # get ID before audit log
|
||||
|
||||
_log_audit(
|
||||
db,
|
||||
action="CREATE",
|
||||
entity_type="activity",
|
||||
entity_id=act.id,
|
||||
new_values={"vvt_id": act.vvt_id, "name": act.name, "status": act.status},
|
||||
)
|
||||
|
||||
db.commit()
|
||||
db.refresh(act)
|
||||
return _activity_to_response(act)
|
||||
|
||||
|
||||
@router.get("/activities/{activity_id}", response_model=VVTActivityResponse)
|
||||
async def get_activity(activity_id: str, db: Session = Depends(get_db)):
|
||||
"""Get a single processing activity by ID."""
|
||||
act = db.query(VVTActivityDB).filter(VVTActivityDB.id == activity_id).first()
|
||||
if not act:
|
||||
raise HTTPException(status_code=404, detail=f"Activity {activity_id} not found")
|
||||
return _activity_to_response(act)
|
||||
|
||||
|
||||
@router.put("/activities/{activity_id}", response_model=VVTActivityResponse)
|
||||
async def update_activity(
|
||||
activity_id: str,
|
||||
request: VVTActivityUpdate,
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Update a processing activity."""
|
||||
act = db.query(VVTActivityDB).filter(VVTActivityDB.id == activity_id).first()
|
||||
if not act:
|
||||
raise HTTPException(status_code=404, detail=f"Activity {activity_id} not found")
|
||||
|
||||
old_values = {"name": act.name, "status": act.status}
|
||||
updates = request.dict(exclude_none=True)
|
||||
for field, value in updates.items():
|
||||
setattr(act, field, value)
|
||||
act.updated_at = datetime.utcnow()
|
||||
|
||||
_log_audit(
|
||||
db,
|
||||
action="UPDATE",
|
||||
entity_type="activity",
|
||||
entity_id=act.id,
|
||||
old_values=old_values,
|
||||
new_values=updates,
|
||||
)
|
||||
|
||||
db.commit()
|
||||
db.refresh(act)
|
||||
return _activity_to_response(act)
|
||||
|
||||
|
||||
@router.delete("/activities/{activity_id}")
|
||||
async def delete_activity(activity_id: str, db: Session = Depends(get_db)):
|
||||
"""Delete a processing activity."""
|
||||
act = db.query(VVTActivityDB).filter(VVTActivityDB.id == activity_id).first()
|
||||
if not act:
|
||||
raise HTTPException(status_code=404, detail=f"Activity {activity_id} not found")
|
||||
|
||||
_log_audit(
|
||||
db,
|
||||
action="DELETE",
|
||||
entity_type="activity",
|
||||
entity_id=act.id,
|
||||
old_values={"vvt_id": act.vvt_id, "name": act.name},
|
||||
)
|
||||
|
||||
db.delete(act)
|
||||
db.commit()
|
||||
return {"success": True, "message": f"Activity {activity_id} deleted"}
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Audit Log
|
||||
# ============================================================================
|
||||
|
||||
@router.get("/audit-log", response_model=List[VVTAuditLogEntry])
|
||||
async def get_audit_log(
|
||||
limit: int = Query(50, ge=1, le=500),
|
||||
offset: int = Query(0, ge=0),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Get the VVT audit trail."""
|
||||
entries = (
|
||||
db.query(VVTAuditLogDB)
|
||||
.order_by(VVTAuditLogDB.created_at.desc())
|
||||
.offset(offset)
|
||||
.limit(limit)
|
||||
.all()
|
||||
)
|
||||
return [
|
||||
VVTAuditLogEntry(
|
||||
id=str(e.id),
|
||||
action=e.action,
|
||||
entity_type=e.entity_type,
|
||||
entity_id=str(e.entity_id) if e.entity_id else None,
|
||||
changed_by=e.changed_by,
|
||||
old_values=e.old_values,
|
||||
new_values=e.new_values,
|
||||
created_at=e.created_at,
|
||||
)
|
||||
for e in entries
|
||||
]
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Export & Stats
|
||||
# ============================================================================
|
||||
|
||||
@router.get("/export")
|
||||
async def export_activities(db: Session = Depends(get_db)):
|
||||
"""JSON export of all activities for external review / PDF generation."""
|
||||
org = db.query(VVTOrganizationDB).order_by(VVTOrganizationDB.created_at).first()
|
||||
activities = db.query(VVTActivityDB).order_by(VVTActivityDB.created_at).all()
|
||||
|
||||
_log_audit(
|
||||
db,
|
||||
action="EXPORT",
|
||||
entity_type="all_activities",
|
||||
new_values={"count": len(activities)},
|
||||
)
|
||||
db.commit()
|
||||
|
||||
return {
|
||||
"exported_at": datetime.utcnow().isoformat(),
|
||||
"organization": {
|
||||
"name": org.organization_name if org else "",
|
||||
"dpo_name": org.dpo_name if org else "",
|
||||
"dpo_contact": org.dpo_contact if org else "",
|
||||
"vvt_version": org.vvt_version if org else "1.0",
|
||||
} if org else None,
|
||||
"activities": [
|
||||
{
|
||||
"id": str(a.id),
|
||||
"vvt_id": a.vvt_id,
|
||||
"name": a.name,
|
||||
"description": a.description,
|
||||
"status": a.status,
|
||||
"purposes": a.purposes,
|
||||
"legal_bases": a.legal_bases,
|
||||
"data_subject_categories": a.data_subject_categories,
|
||||
"personal_data_categories": a.personal_data_categories,
|
||||
"recipient_categories": a.recipient_categories,
|
||||
"third_country_transfers": a.third_country_transfers,
|
||||
"retention_period": a.retention_period,
|
||||
"dpia_required": a.dpia_required,
|
||||
"protection_level": a.protection_level,
|
||||
"business_function": a.business_function,
|
||||
"responsible": a.responsible,
|
||||
"created_at": a.created_at.isoformat(),
|
||||
"updated_at": a.updated_at.isoformat() if a.updated_at else None,
|
||||
}
|
||||
for a in activities
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
@router.get("/stats", response_model=VVTStatsResponse)
|
||||
async def get_stats(db: Session = Depends(get_db)):
|
||||
"""Get VVT statistics summary."""
|
||||
activities = db.query(VVTActivityDB).all()
|
||||
|
||||
by_status: dict = {}
|
||||
by_bf: dict = {}
|
||||
|
||||
for a in activities:
|
||||
status = a.status or 'DRAFT'
|
||||
bf = a.business_function or 'unknown'
|
||||
by_status[status] = by_status.get(status, 0) + 1
|
||||
by_bf[bf] = by_bf.get(bf, 0) + 1
|
||||
|
||||
return VVTStatsResponse(
|
||||
total=len(activities),
|
||||
by_status=by_status,
|
||||
by_business_function=by_bf,
|
||||
dpia_required_count=sum(1 for a in activities if a.dpia_required),
|
||||
third_country_count=sum(1 for a in activities if a.third_country_transfers),
|
||||
draft_count=by_status.get('DRAFT', 0),
|
||||
approved_count=by_status.get('APPROVED', 0),
|
||||
)
|
||||
109
backend-compliance/compliance/db/vvt_models.py
Normal file
109
backend-compliance/compliance/db/vvt_models.py
Normal file
@@ -0,0 +1,109 @@
|
||||
"""
|
||||
SQLAlchemy models for VVT — Verzeichnis von Verarbeitungstaetigkeiten (Art. 30 DSGVO).
|
||||
|
||||
Tables:
|
||||
- compliance_vvt_organization: Organization header (DSB, version, review dates)
|
||||
- compliance_vvt_activities: Individual processing activities
|
||||
- compliance_vvt_audit_log: Audit trail for all VVT changes
|
||||
"""
|
||||
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
|
||||
from sqlalchemy import (
|
||||
Column, String, Text, Boolean, Integer, Date, DateTime, JSON, Index
|
||||
)
|
||||
from sqlalchemy.dialects.postgresql import UUID
|
||||
|
||||
from classroom_engine.database import Base
|
||||
|
||||
|
||||
class VVTOrganizationDB(Base):
|
||||
"""VVT organization header — stores DSB contact, version and review schedule."""
|
||||
|
||||
__tablename__ = 'compliance_vvt_organization'
|
||||
|
||||
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||
organization_name = Column(String(300), nullable=False)
|
||||
industry = Column(String(100))
|
||||
locations = Column(JSON, default=list)
|
||||
employee_count = Column(Integer)
|
||||
dpo_name = Column(String(200))
|
||||
dpo_contact = Column(String(200))
|
||||
vvt_version = Column(String(20), default='1.0')
|
||||
last_review_date = Column(Date)
|
||||
next_review_date = Column(Date)
|
||||
review_interval = Column(String(20), default='annual')
|
||||
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
|
||||
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||
|
||||
__table_args__ = (
|
||||
Index('idx_vvt_org_created', 'created_at'),
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
return f"<VVTOrganization {self.organization_name}>"
|
||||
|
||||
|
||||
class VVTActivityDB(Base):
|
||||
"""Individual processing activity per Art. 30 DSGVO."""
|
||||
|
||||
__tablename__ = 'compliance_vvt_activities'
|
||||
|
||||
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||
vvt_id = Column(String(50), unique=True, nullable=False)
|
||||
name = Column(String(300), nullable=False)
|
||||
description = Column(Text)
|
||||
purposes = Column(JSON, default=list)
|
||||
legal_bases = Column(JSON, default=list)
|
||||
data_subject_categories = Column(JSON, default=list)
|
||||
personal_data_categories = Column(JSON, default=list)
|
||||
recipient_categories = Column(JSON, default=list)
|
||||
third_country_transfers = Column(JSON, default=list)
|
||||
retention_period = Column(JSON, default=dict)
|
||||
tom_description = Column(Text)
|
||||
business_function = Column(String(50))
|
||||
systems = Column(JSON, default=list)
|
||||
deployment_model = Column(String(20))
|
||||
data_sources = Column(JSON, default=list)
|
||||
data_flows = Column(JSON, default=list)
|
||||
protection_level = Column(String(10), default='MEDIUM')
|
||||
dpia_required = Column(Boolean, default=False)
|
||||
structured_toms = Column(JSON, default=dict)
|
||||
status = Column(String(20), default='DRAFT')
|
||||
responsible = Column(String(200))
|
||||
owner = Column(String(200))
|
||||
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
|
||||
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||
|
||||
__table_args__ = (
|
||||
Index('idx_vvt_activities_status', 'status'),
|
||||
Index('idx_vvt_activities_business_function', 'business_function'),
|
||||
Index('idx_vvt_activities_vvt_id', 'vvt_id'),
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
return f"<VVTActivity {self.vvt_id}: {self.name}>"
|
||||
|
||||
|
||||
class VVTAuditLogDB(Base):
|
||||
"""Audit trail for all VVT create/update/delete/export actions."""
|
||||
|
||||
__tablename__ = 'compliance_vvt_audit_log'
|
||||
|
||||
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||
action = Column(String(20), nullable=False) # CREATE, UPDATE, DELETE, EXPORT
|
||||
entity_type = Column(String(50), nullable=False) # activity, organization
|
||||
entity_id = Column(UUID(as_uuid=True))
|
||||
changed_by = Column(String(200))
|
||||
old_values = Column(JSON)
|
||||
new_values = Column(JSON)
|
||||
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
|
||||
|
||||
__table_args__ = (
|
||||
Index('idx_vvt_audit_created', 'created_at'),
|
||||
Index('idx_vvt_audit_entity', 'entity_type', 'entity_id'),
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
return f"<VVTAuditLog {self.action} {self.entity_type}>"
|
||||
66
backend-compliance/migrations/006_vvt.sql
Normal file
66
backend-compliance/migrations/006_vvt.sql
Normal file
@@ -0,0 +1,66 @@
|
||||
-- =========================================================
|
||||
-- Migration 006: VVT — Verzeichnis von Verarbeitungstaetigkeiten
|
||||
-- Art. 30 DSGVO
|
||||
-- =========================================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS compliance_vvt_organization (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
organization_name VARCHAR(300) NOT NULL,
|
||||
industry VARCHAR(100),
|
||||
locations JSONB DEFAULT '[]',
|
||||
employee_count INT,
|
||||
dpo_name VARCHAR(200),
|
||||
dpo_contact VARCHAR(200),
|
||||
vvt_version VARCHAR(20) DEFAULT '1.0',
|
||||
last_review_date DATE,
|
||||
next_review_date DATE,
|
||||
review_interval VARCHAR(20) DEFAULT 'annual',
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS compliance_vvt_activities (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
vvt_id VARCHAR(50) UNIQUE NOT NULL,
|
||||
name VARCHAR(300) NOT NULL,
|
||||
description TEXT,
|
||||
purposes JSONB DEFAULT '[]',
|
||||
legal_bases JSONB DEFAULT '[]',
|
||||
data_subject_categories JSONB DEFAULT '[]',
|
||||
personal_data_categories JSONB DEFAULT '[]',
|
||||
recipient_categories JSONB DEFAULT '[]',
|
||||
third_country_transfers JSONB DEFAULT '[]',
|
||||
retention_period JSONB DEFAULT '{}',
|
||||
tom_description TEXT,
|
||||
business_function VARCHAR(50),
|
||||
systems JSONB DEFAULT '[]',
|
||||
deployment_model VARCHAR(20),
|
||||
data_sources JSONB DEFAULT '[]',
|
||||
data_flows JSONB DEFAULT '[]',
|
||||
protection_level VARCHAR(10) DEFAULT 'MEDIUM',
|
||||
dpia_required BOOLEAN DEFAULT FALSE,
|
||||
structured_toms JSONB DEFAULT '{}',
|
||||
status VARCHAR(20) DEFAULT 'DRAFT',
|
||||
responsible VARCHAR(200),
|
||||
owner VARCHAR(200),
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_vvt_activities_status ON compliance_vvt_activities(status);
|
||||
CREATE INDEX IF NOT EXISTS idx_vvt_activities_business_function ON compliance_vvt_activities(business_function);
|
||||
CREATE INDEX IF NOT EXISTS idx_vvt_activities_vvt_id ON compliance_vvt_activities(vvt_id);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS compliance_vvt_audit_log (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
action VARCHAR(20) NOT NULL,
|
||||
entity_type VARCHAR(50) NOT NULL,
|
||||
entity_id UUID,
|
||||
changed_by VARCHAR(200),
|
||||
old_values JSONB,
|
||||
new_values JSONB,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_vvt_audit_created ON compliance_vvt_audit_log(created_at);
|
||||
CREATE INDEX IF NOT EXISTS idx_vvt_audit_entity ON compliance_vvt_audit_log(entity_type, entity_id);
|
||||
@@ -24,6 +24,7 @@ anthropic==0.75.0
|
||||
|
||||
# PDF Generation (GDPR export, audit reports)
|
||||
weasyprint==66.0
|
||||
reportlab==4.2.5
|
||||
Jinja2==3.1.6
|
||||
|
||||
# Document Processing (Word import for consent admin)
|
||||
|
||||
347
backend-compliance/tests/test_source_policy_routes.py
Normal file
347
backend-compliance/tests/test_source_policy_routes.py
Normal file
@@ -0,0 +1,347 @@
|
||||
"""Tests for Source Policy Router (source_policy_router.py).
|
||||
|
||||
Fokus: Neue Filter-Parameter source_type (list_sources) und category (list_pii_rules)
|
||||
sowie Schema-Validierungen und Audit-Log-Helper.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from unittest.mock import MagicMock, patch, call
|
||||
from datetime import datetime
|
||||
import uuid
|
||||
|
||||
from compliance.api.source_policy_router import (
|
||||
SourceCreate,
|
||||
SourceUpdate,
|
||||
PIIRuleCreate,
|
||||
PIIRuleUpdate,
|
||||
_log_audit,
|
||||
)
|
||||
from compliance.db.source_policy_models import (
|
||||
AllowedSourceDB,
|
||||
PIIRuleDB,
|
||||
SourcePolicyAuditDB,
|
||||
)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Schema Tests: SourceCreate
|
||||
# =============================================================================
|
||||
|
||||
class TestSourceCreate:
|
||||
def test_default_values(self):
|
||||
req = SourceCreate(domain="eur-lex.europa.eu", name="EUR-Lex")
|
||||
assert req.domain == "eur-lex.europa.eu"
|
||||
assert req.name == "EUR-Lex"
|
||||
assert req.source_type == "legal"
|
||||
assert req.active is True
|
||||
assert req.trust_boost == 0.5
|
||||
|
||||
def test_legal_source_type(self):
|
||||
req = SourceCreate(domain="gesetze.de", name="Gesetze.de", source_type="legal")
|
||||
assert req.source_type == "legal"
|
||||
|
||||
def test_guidance_source_type(self):
|
||||
req = SourceCreate(domain="dsb.gv.at", name="DSB Austria", source_type="guidance")
|
||||
assert req.source_type == "guidance"
|
||||
|
||||
def test_technical_source_type(self):
|
||||
req = SourceCreate(domain="bsi.bund.de", name="BSI", source_type="technical")
|
||||
assert req.source_type == "technical"
|
||||
|
||||
def test_trust_boost_range_low(self):
|
||||
req = SourceCreate(domain="example.com", name="Test", trust_boost=0.0)
|
||||
assert req.trust_boost == 0.0
|
||||
|
||||
def test_trust_boost_range_high(self):
|
||||
req = SourceCreate(domain="example.com", name="Test", trust_boost=1.0)
|
||||
assert req.trust_boost == 1.0
|
||||
|
||||
def test_trust_boost_invalid_raises(self):
|
||||
with pytest.raises(Exception):
|
||||
SourceCreate(domain="example.com", name="Test", trust_boost=1.5)
|
||||
|
||||
def test_optional_fields_none(self):
|
||||
req = SourceCreate(domain="example.com", name="Test")
|
||||
assert req.description is None
|
||||
assert req.license is None
|
||||
assert req.legal_basis is None
|
||||
assert req.metadata is None
|
||||
|
||||
def test_full_values(self):
|
||||
req = SourceCreate(
|
||||
domain="eur-lex.europa.eu",
|
||||
name="EUR-Lex",
|
||||
description="EU-Rechtsquellen",
|
||||
license="CC-BY",
|
||||
legal_basis="Art. 5 DSGVO",
|
||||
trust_boost=0.9,
|
||||
source_type="legal",
|
||||
active=True,
|
||||
metadata={"region": "EU"},
|
||||
)
|
||||
assert req.trust_boost == 0.9
|
||||
assert req.metadata == {"region": "EU"}
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Schema Tests: SourceUpdate
|
||||
# =============================================================================
|
||||
|
||||
class TestSourceUpdate:
|
||||
def test_partial_update_source_type(self):
|
||||
req = SourceUpdate(source_type="guidance")
|
||||
data = req.model_dump(exclude_none=True)
|
||||
assert data == {"source_type": "guidance"}
|
||||
|
||||
def test_partial_update_active(self):
|
||||
req = SourceUpdate(active=False)
|
||||
data = req.model_dump(exclude_none=True)
|
||||
assert data == {"active": False}
|
||||
|
||||
def test_empty_update(self):
|
||||
req = SourceUpdate()
|
||||
data = req.model_dump(exclude_none=True)
|
||||
assert data == {}
|
||||
|
||||
def test_multi_field_update(self):
|
||||
req = SourceUpdate(source_type="technical", trust_boost=0.8, active=True)
|
||||
data = req.model_dump(exclude_none=True)
|
||||
assert data["source_type"] == "technical"
|
||||
assert data["trust_boost"] == 0.8
|
||||
assert data["active"] is True
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Schema Tests: PIIRuleCreate
|
||||
# =============================================================================
|
||||
|
||||
class TestPIIRuleCreate:
|
||||
def test_default_values(self):
|
||||
req = PIIRuleCreate(name="E-Mail-Erkennung", category="pii")
|
||||
assert req.name == "E-Mail-Erkennung"
|
||||
assert req.category == "pii"
|
||||
assert req.action == "mask"
|
||||
assert req.active is True
|
||||
assert req.pattern is None
|
||||
|
||||
def test_financial_category(self):
|
||||
req = PIIRuleCreate(name="IBAN", category="financial", pattern=r"DE\d{20}")
|
||||
assert req.category == "financial"
|
||||
assert req.pattern == r"DE\d{20}"
|
||||
|
||||
def test_health_category(self):
|
||||
req = PIIRuleCreate(name="Diagnose", category="health")
|
||||
assert req.category == "health"
|
||||
|
||||
def test_id_category(self):
|
||||
req = PIIRuleCreate(name="Personalausweis", category="id")
|
||||
assert req.category == "id"
|
||||
|
||||
def test_action_redact(self):
|
||||
req = PIIRuleCreate(name="Test", category="pii", action="redact")
|
||||
assert req.action == "redact"
|
||||
|
||||
def test_serialization(self):
|
||||
req = PIIRuleCreate(name="Telefon", category="pii", pattern=r"\+49\d+")
|
||||
data = req.model_dump()
|
||||
assert data["name"] == "Telefon"
|
||||
assert data["category"] == "pii"
|
||||
assert data["pattern"] == r"\+49\d+"
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Schema Tests: PIIRuleUpdate
|
||||
# =============================================================================
|
||||
|
||||
class TestPIIRuleUpdate:
|
||||
def test_partial_update_category(self):
|
||||
req = PIIRuleUpdate(category="financial")
|
||||
data = req.model_dump(exclude_none=True)
|
||||
assert data == {"category": "financial"}
|
||||
|
||||
def test_partial_update_active(self):
|
||||
req = PIIRuleUpdate(active=False)
|
||||
data = req.model_dump(exclude_none=True)
|
||||
assert data == {"active": False}
|
||||
|
||||
def test_empty_update(self):
|
||||
req = PIIRuleUpdate()
|
||||
data = req.model_dump(exclude_none=True)
|
||||
assert data == {}
|
||||
|
||||
def test_multi_field_update(self):
|
||||
req = PIIRuleUpdate(name="Updated", category="id", action="redact")
|
||||
data = req.model_dump(exclude_none=True)
|
||||
assert data["name"] == "Updated"
|
||||
assert data["category"] == "id"
|
||||
assert data["action"] == "redact"
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# DB Model Tests: AllowedSourceDB
|
||||
# =============================================================================
|
||||
|
||||
class TestAllowedSourceDB:
|
||||
def test_default_source_type(self):
|
||||
src = AllowedSourceDB(
|
||||
id=uuid.uuid4(),
|
||||
domain="example.com",
|
||||
name="Test Source",
|
||||
)
|
||||
# Column default is 'legal'
|
||||
assert src.__tablename__ == 'compliance_allowed_sources'
|
||||
|
||||
def test_repr(self):
|
||||
src = AllowedSourceDB(domain="bsi.bund.de", name="BSI")
|
||||
assert "bsi.bund.de" in repr(src)
|
||||
assert "BSI" in repr(src)
|
||||
|
||||
def test_tablename(self):
|
||||
assert AllowedSourceDB.__tablename__ == 'compliance_allowed_sources'
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# DB Model Tests: PIIRuleDB
|
||||
# =============================================================================
|
||||
|
||||
class TestPIIRuleDB:
|
||||
def test_tablename(self):
|
||||
assert PIIRuleDB.__tablename__ == 'compliance_pii_rules'
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Filter Logic Tests (Unit — Mock DB)
|
||||
# =============================================================================
|
||||
|
||||
class TestSourceTypeFilter:
|
||||
"""Tests that list_sources correctly applies the source_type filter."""
|
||||
|
||||
def test_source_type_filter_applied(self):
|
||||
"""source_type param should be passed to DB query filter."""
|
||||
db_mock = MagicMock()
|
||||
query_mock = MagicMock()
|
||||
db_mock.query.return_value = query_mock
|
||||
query_mock.filter.return_value = query_mock
|
||||
query_mock.order_by.return_value = query_mock
|
||||
query_mock.offset.return_value = query_mock
|
||||
query_mock.limit.return_value = query_mock
|
||||
query_mock.all.return_value = []
|
||||
|
||||
# Simulate filter call chain for source_type='legal'
|
||||
filtered = query_mock.filter.return_value
|
||||
filtered.filter.return_value = filtered
|
||||
filtered.order_by.return_value = filtered
|
||||
filtered.offset.return_value = filtered
|
||||
filtered.limit.return_value = filtered
|
||||
filtered.all.return_value = []
|
||||
|
||||
# Verify filter is called when source_type is provided
|
||||
result = db_mock.query(AllowedSourceDB)
|
||||
result = result.filter(AllowedSourceDB.source_type == "legal")
|
||||
assert query_mock.filter.call_count == 1
|
||||
|
||||
def test_no_filter_without_source_type(self):
|
||||
"""Without source_type param, no filter should be applied."""
|
||||
db_mock = MagicMock()
|
||||
query_mock = MagicMock()
|
||||
db_mock.query.return_value = query_mock
|
||||
query_mock.order_by.return_value = query_mock
|
||||
query_mock.offset.return_value = query_mock
|
||||
query_mock.limit.return_value = query_mock
|
||||
query_mock.all.return_value = []
|
||||
|
||||
# Without filter
|
||||
result = db_mock.query(AllowedSourceDB)
|
||||
result = result.order_by(AllowedSourceDB.name)
|
||||
# filter NOT called → count should be 0
|
||||
assert query_mock.filter.call_count == 0
|
||||
|
||||
|
||||
class TestCategoryFilter:
|
||||
"""Tests that list_pii_rules correctly applies the category filter."""
|
||||
|
||||
def test_category_filter_applied(self):
|
||||
"""category param should be passed to DB query filter."""
|
||||
db_mock = MagicMock()
|
||||
query_mock = MagicMock()
|
||||
db_mock.query.return_value = query_mock
|
||||
query_mock.filter.return_value = query_mock
|
||||
query_mock.order_by.return_value = query_mock
|
||||
query_mock.all.return_value = []
|
||||
|
||||
# Simulate filter for category='financial'
|
||||
result = db_mock.query(PIIRuleDB)
|
||||
result = result.filter(PIIRuleDB.category == "financial")
|
||||
assert query_mock.filter.call_count == 1
|
||||
|
||||
def test_category_values(self):
|
||||
"""All valid category values should be accepted by PIIRuleCreate."""
|
||||
categories = ["pii", "financial", "health", "id", "location", "other"]
|
||||
for cat in categories:
|
||||
req = PIIRuleCreate(name=f"Rule {cat}", category=cat)
|
||||
assert req.category == cat
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Audit Log Helper Tests
|
||||
# =============================================================================
|
||||
|
||||
class TestLogAudit:
|
||||
def test_creates_audit_entry(self):
|
||||
db_mock = MagicMock()
|
||||
entity_id = uuid.uuid4()
|
||||
|
||||
_log_audit(
|
||||
db_mock,
|
||||
action="create",
|
||||
entity_type="source",
|
||||
entity_id=entity_id,
|
||||
new_values={"name": "Test Source", "domain": "example.com"},
|
||||
)
|
||||
|
||||
db_mock.add.assert_called_once()
|
||||
audit_obj = db_mock.add.call_args[0][0]
|
||||
assert isinstance(audit_obj, SourcePolicyAuditDB)
|
||||
assert audit_obj.action == "create"
|
||||
assert audit_obj.entity_type == "source"
|
||||
|
||||
def test_creates_audit_entry_with_old_values(self):
|
||||
db_mock = MagicMock()
|
||||
entity_id = uuid.uuid4()
|
||||
|
||||
_log_audit(
|
||||
db_mock,
|
||||
action="update",
|
||||
entity_type="source",
|
||||
entity_id=entity_id,
|
||||
old_values={"name": "Old Name"},
|
||||
new_values={"name": "New Name"},
|
||||
)
|
||||
|
||||
audit_obj = db_mock.add.call_args[0][0]
|
||||
assert audit_obj.action == "update"
|
||||
assert audit_obj.old_values == {"name": "Old Name"}
|
||||
assert audit_obj.new_values == {"name": "New Name"}
|
||||
|
||||
def test_creates_audit_entry_for_delete(self):
|
||||
db_mock = MagicMock()
|
||||
entity_id = uuid.uuid4()
|
||||
|
||||
_log_audit(
|
||||
db_mock,
|
||||
action="delete",
|
||||
entity_type="pii_rule",
|
||||
entity_id=entity_id,
|
||||
old_values={"name": "Deleted Rule"},
|
||||
)
|
||||
|
||||
audit_obj = db_mock.add.call_args[0][0]
|
||||
assert audit_obj.action == "delete"
|
||||
assert audit_obj.entity_type == "pii_rule"
|
||||
|
||||
def test_add_called_without_commit(self):
|
||||
"""_log_audit calls db.add() but NOT db.commit() — commit happens at the endpoint level."""
|
||||
db_mock = MagicMock()
|
||||
_log_audit(db_mock, "create", "source", uuid.uuid4())
|
||||
db_mock.add.assert_called_once()
|
||||
db_mock.commit.assert_not_called()
|
||||
222
backend-compliance/tests/test_vvt_routes.py
Normal file
222
backend-compliance/tests/test_vvt_routes.py
Normal file
@@ -0,0 +1,222 @@
|
||||
"""Tests for VVT routes and schemas (vvt_routes.py, vvt_models.py)."""
|
||||
|
||||
import pytest
|
||||
from unittest.mock import MagicMock, patch
|
||||
from datetime import datetime, date
|
||||
import uuid
|
||||
|
||||
from compliance.api.schemas import (
|
||||
VVTActivityCreate,
|
||||
VVTActivityUpdate,
|
||||
VVTOrganizationUpdate,
|
||||
VVTStatsResponse,
|
||||
)
|
||||
from compliance.api.vvt_routes import _activity_to_response, _log_audit
|
||||
from compliance.db.vvt_models import VVTActivityDB, VVTOrganizationDB, VVTAuditLogDB
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Schema Tests
|
||||
# =============================================================================
|
||||
|
||||
class TestVVTActivityCreate:
|
||||
def test_default_values(self):
|
||||
req = VVTActivityCreate(vvt_id="VVT-001", name="Test Verarbeitung")
|
||||
assert req.vvt_id == "VVT-001"
|
||||
assert req.name == "Test Verarbeitung"
|
||||
assert req.status == "DRAFT"
|
||||
assert req.protection_level == "MEDIUM"
|
||||
assert req.dpia_required is False
|
||||
assert req.purposes == []
|
||||
assert req.legal_bases == []
|
||||
|
||||
def test_full_values(self):
|
||||
req = VVTActivityCreate(
|
||||
vvt_id="VVT-002",
|
||||
name="Gehaltsabrechnung",
|
||||
description="Verarbeitung von Gehaltsabrechnungsdaten",
|
||||
purposes=["Vertragserfuellung"],
|
||||
legal_bases=["Art. 6 Abs. 1b DSGVO"],
|
||||
data_subject_categories=["Mitarbeiter"],
|
||||
personal_data_categories=["Bankdaten", "Steuer-ID"],
|
||||
status="APPROVED",
|
||||
dpia_required=False,
|
||||
)
|
||||
assert req.vvt_id == "VVT-002"
|
||||
assert req.status == "APPROVED"
|
||||
assert len(req.purposes) == 1
|
||||
assert len(req.personal_data_categories) == 2
|
||||
|
||||
def test_serialization(self):
|
||||
req = VVTActivityCreate(vvt_id="VVT-003", name="Test")
|
||||
data = req.model_dump()
|
||||
assert data["vvt_id"] == "VVT-003"
|
||||
assert isinstance(data["purposes"], list)
|
||||
assert isinstance(data["retention_period"], dict)
|
||||
|
||||
|
||||
class TestVVTActivityUpdate:
|
||||
def test_partial_update(self):
|
||||
req = VVTActivityUpdate(status="APPROVED")
|
||||
data = req.model_dump(exclude_none=True)
|
||||
assert data == {"status": "APPROVED"}
|
||||
|
||||
def test_empty_update(self):
|
||||
req = VVTActivityUpdate()
|
||||
data = req.model_dump(exclude_none=True)
|
||||
assert data == {}
|
||||
|
||||
def test_multi_field_update(self):
|
||||
req = VVTActivityUpdate(
|
||||
name="Updated Name",
|
||||
dpia_required=True,
|
||||
protection_level="HIGH",
|
||||
)
|
||||
data = req.model_dump(exclude_none=True)
|
||||
assert data["name"] == "Updated Name"
|
||||
assert data["dpia_required"] is True
|
||||
assert data["protection_level"] == "HIGH"
|
||||
|
||||
|
||||
class TestVVTOrganizationUpdate:
|
||||
def test_defaults(self):
|
||||
req = VVTOrganizationUpdate()
|
||||
data = req.model_dump(exclude_none=True)
|
||||
assert data == {}
|
||||
|
||||
def test_partial_update(self):
|
||||
req = VVTOrganizationUpdate(
|
||||
organization_name="BreakPilot GmbH",
|
||||
dpo_name="Max Mustermann",
|
||||
)
|
||||
data = req.model_dump(exclude_none=True)
|
||||
assert data["organization_name"] == "BreakPilot GmbH"
|
||||
assert data["dpo_name"] == "Max Mustermann"
|
||||
|
||||
|
||||
class TestVVTStatsResponse:
|
||||
def test_stats_response(self):
|
||||
stats = VVTStatsResponse(
|
||||
total=5,
|
||||
by_status={"DRAFT": 3, "APPROVED": 2},
|
||||
by_business_function={"HR": 2, "IT": 3},
|
||||
dpia_required_count=1,
|
||||
third_country_count=0,
|
||||
draft_count=3,
|
||||
approved_count=2,
|
||||
)
|
||||
assert stats.total == 5
|
||||
assert stats.by_status["DRAFT"] == 3
|
||||
assert stats.dpia_required_count == 1
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# DB Model Tests
|
||||
# =============================================================================
|
||||
|
||||
class TestVVTModels:
|
||||
def test_activity_defaults(self):
|
||||
act = VVTActivityDB()
|
||||
assert act.status is None or act.status == 'DRAFT'
|
||||
assert act.dpia_required is False or act.dpia_required is None
|
||||
|
||||
def test_activity_repr(self):
|
||||
act = VVTActivityDB()
|
||||
act.vvt_id = "VVT-001"
|
||||
act.name = "Test"
|
||||
assert "VVT-001" in repr(act)
|
||||
|
||||
def test_organization_repr(self):
|
||||
org = VVTOrganizationDB()
|
||||
org.organization_name = "Test GmbH"
|
||||
assert "Test GmbH" in repr(org)
|
||||
|
||||
def test_audit_log_repr(self):
|
||||
log = VVTAuditLogDB()
|
||||
log.action = "CREATE"
|
||||
log.entity_type = "activity"
|
||||
assert "CREATE" in repr(log)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Helper Function Tests
|
||||
# =============================================================================
|
||||
|
||||
class TestActivityToResponse:
|
||||
def _make_activity(self, **kwargs) -> VVTActivityDB:
|
||||
act = VVTActivityDB()
|
||||
act.id = uuid.uuid4()
|
||||
act.vvt_id = kwargs.get("vvt_id", "VVT-001")
|
||||
act.name = kwargs.get("name", "Test")
|
||||
act.description = kwargs.get("description", None)
|
||||
act.purposes = kwargs.get("purposes", [])
|
||||
act.legal_bases = kwargs.get("legal_bases", [])
|
||||
act.data_subject_categories = kwargs.get("data_subject_categories", [])
|
||||
act.personal_data_categories = kwargs.get("personal_data_categories", [])
|
||||
act.recipient_categories = kwargs.get("recipient_categories", [])
|
||||
act.third_country_transfers = kwargs.get("third_country_transfers", [])
|
||||
act.retention_period = kwargs.get("retention_period", {})
|
||||
act.tom_description = kwargs.get("tom_description", None)
|
||||
act.business_function = kwargs.get("business_function", None)
|
||||
act.systems = kwargs.get("systems", [])
|
||||
act.deployment_model = kwargs.get("deployment_model", None)
|
||||
act.data_sources = kwargs.get("data_sources", [])
|
||||
act.data_flows = kwargs.get("data_flows", [])
|
||||
act.protection_level = kwargs.get("protection_level", "MEDIUM")
|
||||
act.dpia_required = kwargs.get("dpia_required", False)
|
||||
act.structured_toms = kwargs.get("structured_toms", {})
|
||||
act.status = kwargs.get("status", "DRAFT")
|
||||
act.responsible = kwargs.get("responsible", None)
|
||||
act.owner = kwargs.get("owner", None)
|
||||
act.created_at = datetime.utcnow()
|
||||
act.updated_at = None
|
||||
return act
|
||||
|
||||
def test_basic_conversion(self):
|
||||
act = self._make_activity(vvt_id="VVT-001", name="Kundendaten")
|
||||
response = _activity_to_response(act)
|
||||
assert response.vvt_id == "VVT-001"
|
||||
assert response.name == "Kundendaten"
|
||||
assert response.status == "DRAFT"
|
||||
assert response.protection_level == "MEDIUM"
|
||||
|
||||
def test_null_lists_become_empty(self):
|
||||
act = self._make_activity()
|
||||
act.purposes = None
|
||||
act.legal_bases = None
|
||||
response = _activity_to_response(act)
|
||||
assert response.purposes == []
|
||||
assert response.legal_bases == []
|
||||
|
||||
def test_null_dicts_become_empty(self):
|
||||
act = self._make_activity()
|
||||
act.retention_period = None
|
||||
act.structured_toms = None
|
||||
response = _activity_to_response(act)
|
||||
assert response.retention_period == {}
|
||||
assert response.structured_toms == {}
|
||||
|
||||
|
||||
class TestLogAudit:
|
||||
def test_creates_audit_entry(self):
|
||||
mock_db = MagicMock()
|
||||
act_id = uuid.uuid4()
|
||||
_log_audit(
|
||||
db=mock_db,
|
||||
action="CREATE",
|
||||
entity_type="activity",
|
||||
entity_id=act_id,
|
||||
changed_by="test_user",
|
||||
new_values={"name": "Test"},
|
||||
)
|
||||
mock_db.add.assert_called_once()
|
||||
added = mock_db.add.call_args[0][0]
|
||||
assert added.action == "CREATE"
|
||||
assert added.entity_type == "activity"
|
||||
assert added.entity_id == act_id
|
||||
|
||||
def test_defaults_changed_by(self):
|
||||
mock_db = MagicMock()
|
||||
_log_audit(mock_db, "DELETE", "activity")
|
||||
added = mock_db.add.call_args[0][0]
|
||||
assert added.changed_by == "system"
|
||||
318
docs-src/services/sdk-modules/dokumentations-module.md
Normal file
318
docs-src/services/sdk-modules/dokumentations-module.md
Normal file
@@ -0,0 +1,318 @@
|
||||
# Dokumentations-Module (Paket 3 + ergänzende Module)
|
||||
|
||||
Diese Seite beschreibt die sechs Module, die die Compliance-Dokumentation vervollständigen:
|
||||
**VVT**, **Training**, **Source Policy**, **Document Generator**, **Audit Checklist** und **Audit Report**.
|
||||
Alle Module sind vollständig backend-persistent und bieten CRUD-Operationen über die REST-API.
|
||||
|
||||
---
|
||||
|
||||
## Übersicht
|
||||
|
||||
| Modul | SDK-Route | Paket | Checkpoint | Status |
|
||||
|-------|-----------|-------|-----------|--------|
|
||||
| [VVT](#vvt) | `/sdk/vvt` | Dokumentation | CP-VVT (REQUIRED / DSB) | 100% |
|
||||
| [Source Policy](#source-policy) | `/sdk/source-policy` | Vorbereitung | CP-SPOL (REQUIRED) | 100% |
|
||||
| [Document Generator](#document-generator) | `/sdk/document-generator` | Rechtliche Texte | CP-DOCGEN (RECOMMENDED) | 100% |
|
||||
| [Audit Checklist](#audit-checklist) | `/sdk/audit-checklist` | Analyse | CP-CHK (RECOMMENDED) | 100% |
|
||||
| [Audit Report](#audit-report) | `/sdk/audit-report` | Analyse | CP-AREP (REQUIRED) | 100% |
|
||||
| [Training Engine](#training-engine) | `/sdk/training` | Betrieb | CP-TRAIN (REQUIRED) | 100% |
|
||||
|
||||
---
|
||||
|
||||
## VVT
|
||||
|
||||
**Route:** `/sdk/vvt` | **Backend:** `backend-compliance:8002` | **Rechtsgrundlage:** Art. 30 DSGVO
|
||||
|
||||
### Funktionen
|
||||
|
||||
- Anlegen, Bearbeiten und Löschen von Verarbeitungstätigkeiten (CRUD)
|
||||
- Eindeutiger VVT-ID pro Tätigkeit (z.B. `VVT-001`)
|
||||
- Vollständige Dokumentation je Tätigkeit: Zweck, Rechtsgrundlage, Datenkategorien, Empfänger, Drittlandtransfers, Löschfristen, TOMs
|
||||
- Organisationsweite Metadaten: DSB-Kontakt, Branche, Standorte, Mitarbeiterzahl
|
||||
- Audit-Log für alle Änderungen (CREATE / UPDATE / DELETE)
|
||||
- Statistik-Endpoint: Gesamt, nach Status, nach Geschäftsfunktion, DSFA-pflichtige Tätigkeiten
|
||||
- JSON-Export aller Aktivitäten
|
||||
- Status-Workflow: `DRAFT` → `REVIEW` → `APPROVED` → `ARCHIVED`
|
||||
- Schutzstufenklassifizierung: `LOW` / `MEDIUM` / `HIGH` / `CRITICAL`
|
||||
- Profiling-Assistent (UI-seitig) zur Vorbefüllung
|
||||
|
||||
### API-Endpoints
|
||||
|
||||
| Methode | Pfad | Beschreibung |
|
||||
|---------|------|--------------|
|
||||
| `GET` | `/api/compliance/vvt/organization` | Organisationsheader laden |
|
||||
| `PUT` | `/api/compliance/vvt/organization` | Organisationsheader speichern |
|
||||
| `GET` | `/api/compliance/vvt/activities` | Alle Tätigkeiten (Filter: status, business_function) |
|
||||
| `POST` | `/api/compliance/vvt/activities` | Neue Tätigkeit anlegen |
|
||||
| `GET` | `/api/compliance/vvt/activities/{id}` | Einzelne Tätigkeit |
|
||||
| `PUT` | `/api/compliance/vvt/activities/{id}` | Tätigkeit aktualisieren |
|
||||
| `DELETE` | `/api/compliance/vvt/activities/{id}` | Tätigkeit löschen |
|
||||
| `GET` | `/api/compliance/vvt/audit-log` | Änderungshistorie |
|
||||
| `GET` | `/api/compliance/vvt/export` | JSON-Export aller Tätigkeiten |
|
||||
| `GET` | `/api/compliance/vvt/stats` | Statistiken |
|
||||
|
||||
### DB-Tabellen
|
||||
|
||||
| Tabelle | Modus | Beschreibung |
|
||||
|---------|-------|--------------|
|
||||
| `compliance_vvt_organization` | read/write | Organisationsweite Metadaten |
|
||||
| `compliance_vvt_activities` | read/write | Verarbeitungstätigkeiten |
|
||||
| `compliance_vvt_audit_log` | write | Änderungsprotokoll |
|
||||
|
||||
### Datenmodell (Aktivität)
|
||||
|
||||
```json
|
||||
{
|
||||
"vvt_id": "VVT-001",
|
||||
"name": "Gehaltsabrechnung",
|
||||
"purposes": ["Vertragserfüllung"],
|
||||
"legal_bases": ["Art. 6 Abs. 1b DSGVO"],
|
||||
"data_subject_categories": ["Mitarbeiter"],
|
||||
"personal_data_categories": ["Bankdaten", "Steuer-ID"],
|
||||
"recipient_categories": ["Steuerberater"],
|
||||
"third_country_transfers": [],
|
||||
"retention_period": {"years": 10, "basis": "§ 257 HGB"},
|
||||
"protection_level": "HIGH",
|
||||
"dpia_required": false,
|
||||
"status": "APPROVED"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Source Policy
|
||||
|
||||
**Route:** `/sdk/source-policy` | **Backend:** `backend-compliance:8002` | **Rechtsgrundlage:** Art. 5 DSGVO
|
||||
|
||||
### Funktionen
|
||||
|
||||
- Verwaltung erlaubter Compliance-Rechtsquellen (Gesetze, Leitlinien, Standards)
|
||||
- Filter nach Quelltyp (`legal`, `guidance`, `template`, `technical`, `other`) und Lizenz
|
||||
- PII-Regelwerk: Definition sensibler Datenkategorien (E-Mail, IBAN, Personalausweis, etc.)
|
||||
- Filter nach PII-Kategorie (pii, financial, health, id, location, other)
|
||||
- Quell-Operations-Matrix: Welche Operationen auf welchen Quellen sind erlaubt
|
||||
- Policy-Audit-Log: Nachvollziehbare Protokollierung aller Policy-Änderungen
|
||||
- Policy-Statistiken: Zusammenfassung des Compliance-Status
|
||||
- Compliance-Report: Gesamtübersicht aller aktiven Quellen und Regeln
|
||||
|
||||
### API-Endpoints
|
||||
|
||||
| Methode | Pfad | Beschreibung |
|
||||
|---------|------|--------------|
|
||||
| `GET` | `/api/source-policy/sources` | Quellen (Filter: source_type, license, active_only) |
|
||||
| `POST` | `/api/source-policy/sources` | Neue Quelle anlegen |
|
||||
| `PUT` | `/api/source-policy/sources/{id}` | Quelle aktualisieren |
|
||||
| `DELETE` | `/api/source-policy/sources/{id}` | Quelle löschen |
|
||||
| `GET` | `/api/source-policy/pii-rules` | PII-Regeln (Filter: category) |
|
||||
| `POST` | `/api/source-policy/pii-rules` | Neue PII-Regel |
|
||||
| `PUT` | `/api/source-policy/pii-rules/{id}` | PII-Regel aktualisieren |
|
||||
| `DELETE` | `/api/source-policy/pii-rules/{id}` | PII-Regel löschen |
|
||||
| `GET` | `/api/source-policy/operations-matrix` | Operations-Matrix |
|
||||
| `GET` | `/api/source-policy/policy-stats` | Statistiken |
|
||||
| `GET` | `/api/source-policy/compliance-report` | Compliance-Report |
|
||||
|
||||
### DB-Tabellen
|
||||
|
||||
| Tabelle | Modus | Beschreibung |
|
||||
|---------|-------|--------------|
|
||||
| `compliance_allowed_sources` | read/write | Erlaubte Rechtsquellen |
|
||||
| `compliance_pii_rules` | read/write | PII-Erkennungsregeln |
|
||||
| `compliance_source_operations` | read/write | Operations-Matrix |
|
||||
| `compliance_source_policy_audit` | write | Audit-Trail |
|
||||
|
||||
---
|
||||
|
||||
## Document Generator
|
||||
|
||||
**Route:** `/sdk/document-generator` | **Backend:** breakpilot-core (Template-Service) | **Rechtsgrundlage:** Art. 28 DSGVO, DDG § 5
|
||||
|
||||
### Funktionen
|
||||
|
||||
- Generierung rechtlicher Dokumente aus Templates: Impressum, AVV, Datenschutzrichtlinie, NDA
|
||||
- Templates werden aus `bp_legal_templates` (RAG-Collection) geladen
|
||||
- Unternehmensspezifische Befüllung aus Company Profile
|
||||
- **PDF-Export** direkt im Browser via `window.print()` — kein Server-seitiger Service erforderlich
|
||||
- Fallback-Banner: Wenn der Template-Service (breakpilot-core) nicht erreichbar ist, erscheint ein informativer Hinweis
|
||||
- Attributionsnachweis: Verwendete Rechtsquellen werden im Dokument aufgeführt
|
||||
|
||||
### Generierte Dokumente
|
||||
|
||||
| Dokument | Rechtsgrundlage | Format |
|
||||
|----------|-----------------|--------|
|
||||
| Impressum | DDG § 5 (ehemals TMG) | HTML / PDF |
|
||||
| Auftragsverarbeitungsvertrag (AVV) | Art. 28 DSGVO | HTML / PDF |
|
||||
| Datenschutzerklärung | Art. 13, 14 DSGVO | HTML / PDF |
|
||||
| NDA / Vertraulichkeitsvereinbarung | GeschGehG | HTML / PDF |
|
||||
|
||||
### PDF-Export
|
||||
|
||||
Der PDF-Export öffnet ein neues Browser-Fenster mit dem vollständig formatierten Dokument
|
||||
und löst automatisch den Browser-Druckdialog aus (`window.print()`). Keine zusätzliche
|
||||
Server-Dependency erforderlich.
|
||||
|
||||
```
|
||||
[Als PDF exportieren] → window.open() → Dokument-HTML → window.print() → Browser-PDF-Dialog
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Audit Checklist
|
||||
|
||||
**Route:** `/sdk/audit-checklist` | **Backend:** `backend-compliance:8002` | **Rechtsgrundlage:** Art. 5 Abs. 2 DSGVO (Rechenschaftspflicht)
|
||||
|
||||
### Funktionen
|
||||
|
||||
- Session-Management: Neue Audit-Sitzung erstellen (Name, Auditor, Scope)
|
||||
- Status-Workflow: `draft` → `in_progress` → `completed` → `archived`
|
||||
- Automatische Befüllung der Checkliste aus Requirements und Controls
|
||||
- Interaktiver Sign-Off-Workflow: Konform / Teilweise / Nicht konform / Nicht geprüft
|
||||
- Digitale Signatur-Hash (SHA-256) pro Prüfpunkt für Unveränderlichkeitsnachweis
|
||||
- Notizen-Bearbeitung je Prüfpunkt
|
||||
- **PDF-Download** in Deutsch oder Englisch (`GET /sessions/{id}/report/pdf`)
|
||||
- JSON-Export der gesamten Checkliste
|
||||
- Session-History: Übersicht vergangener Audit-Sitzungen
|
||||
|
||||
### API-Endpoints
|
||||
|
||||
| Methode | Pfad | Beschreibung |
|
||||
|---------|------|--------------|
|
||||
| `GET` | `/api/compliance/audit/sessions` | Alle Sitzungen |
|
||||
| `POST` | `/api/compliance/audit/sessions` | Neue Sitzung |
|
||||
| `PUT` | `/api/compliance/audit/sessions/{id}` | Sitzung aktualisieren (Status) |
|
||||
| `GET` | `/api/compliance/audit/checklist/{sessionId}` | Checkliste laden |
|
||||
| `PUT` | `/api/compliance/audit/checklist/{sessionId}/items/{reqId}/sign-off` | Prüfpunkt abzeichnen |
|
||||
| `GET` | `/api/compliance/audit/sessions/{sessionId}/report/pdf` | PDF-Report (lang=de/en) |
|
||||
|
||||
!!! warning "Korrekter PDF-Endpunkt"
|
||||
Der PDF-Download-Endpunkt lautet `/sessions/{id}/**report**/pdf`, nicht `/sessions/{id}/pdf`.
|
||||
Dieser Fehler war in früheren Versionen vorhanden und wurde behoben.
|
||||
|
||||
### DB-Tabellen
|
||||
|
||||
| Tabelle | Modus | Beschreibung |
|
||||
|---------|-------|--------------|
|
||||
| `compliance_audit_sessions` | read/write | Audit-Sitzungen |
|
||||
| `compliance_audit_signoffs` | write | Prüfpunkt-Abzeichnungen mit Signatur-Hash |
|
||||
| `compliance_requirements` | read | Prüfpunkte aus Requirements |
|
||||
|
||||
### PDF-Generierung
|
||||
|
||||
Die PDF-Reports werden serverseitig mit **ReportLab 4.2.5** generiert und enthalten:
|
||||
- Deckblatt mit Audit-Metadaten
|
||||
- Zusammenfassung mit Ampelstatus
|
||||
- Statistik-Kreisdiagramm (konform / nicht konform / ausstehend)
|
||||
- Prüfpunkt-Details mit Notizen und digitalem Signatur-Hash
|
||||
- Anhang: Nicht-konforme Punkte mit Handlungsempfehlungen
|
||||
|
||||
---
|
||||
|
||||
## Audit Report
|
||||
|
||||
**Route:** `/sdk/audit-report` und `/sdk/audit-report/{sessionId}` | **Backend:** `backend-compliance:8002` | **Rechtsgrundlage:** Art. 5 Abs. 2 DSGVO
|
||||
|
||||
### Funktionen
|
||||
|
||||
- Übersicht aller Audit-Sitzungen mit Status-Badges (Entwurf / In Bearbeitung / Abgeschlossen / Archiviert)
|
||||
- Detail-Seite pro Sitzung:
|
||||
- Sitzungs-Metadaten (Auditor, Organisation, Zeitraum)
|
||||
- Fortschrittsbalken mit Farbkodierung (grün ≥ 80%, gelb ≥ 50%, rot < 50%)
|
||||
- Statistik-Kacheln (konform / nicht konform / ausstehend)
|
||||
- Interaktive Prüfpunkte mit nachträglichem Sign-Off
|
||||
- Notizen-Bearbeitung per Prüfpunkt
|
||||
- PDF-Download mit Sprachauswahl (DE / EN)
|
||||
- Navigation: Klick auf eine Sitzung in der Übersicht öffnet die Detail-Seite
|
||||
|
||||
### API-Endpoints
|
||||
|
||||
Nutzt dieselben Backend-Endpoints wie Audit Checklist (s.o.).
|
||||
|
||||
### DB-Tabellen
|
||||
|
||||
| Tabelle | Modus | Beschreibung |
|
||||
|---------|-------|--------------|
|
||||
| `compliance_audit_sessions` | read/write | Sitzungsdaten inkl. Fortschritt |
|
||||
| `compliance_audit_signoffs` | read/write | Nachträgliche Prüfpunkt-Aktualisierungen |
|
||||
|
||||
---
|
||||
|
||||
## Training Engine
|
||||
|
||||
**Route:** `/sdk/training` | **Backend:** `ai-compliance-sdk:8093` (Go) | **Rechtsgrundlage:** Art. 39 Abs. 1b DSGVO, EU AI Act Art. 4
|
||||
|
||||
### Funktionen
|
||||
|
||||
- **28 vordefinierte Schulungsmodule** für DSGVO, ISO 27001, AI Act, Hinweisgeberschutz u.a.
|
||||
- Modul-Typen: Jährlich (`annual`), Ereignisbasiert (`event_trigger`), Mikro-Schulung (`micro`)
|
||||
- Quiz-System: Automatisch generierte Fragen mit konfigurierbarer Bestehensgrenze
|
||||
- Zertifikate bei erfolgreichem Abschluss
|
||||
- Schulungsmatrix: Rollen-basierte Pflichtmodulzuordnung (CISO, DSB, Entwickler, etc.)
|
||||
- Aufgabenzuweisung: Schulungen können Mitarbeitern zugewiesen werden
|
||||
- Eskalation: Automatische Erinnerungen bei überfälligen Pflichtschulungen
|
||||
- KI-generierter Content: Schulungsinhalte können via Ollama-LLM automatisch generiert werden
|
||||
- Audit-Log: Vollständige Nachverfolgung aller Schulungsaktivitäten
|
||||
|
||||
### API-Endpoints
|
||||
|
||||
| Methode | Pfad | Beschreibung |
|
||||
|---------|------|--------------|
|
||||
| `GET` | `/sdk/v1/training/modules` | Alle Module (Filter: regulation_area, frequency_type, search) |
|
||||
| `POST` | `/sdk/v1/training/modules` | Neues Modul anlegen |
|
||||
| `GET` | `/sdk/v1/training/modules/{id}` | Modul mit Content und Quiz-Fragen |
|
||||
| `PUT` | `/sdk/v1/training/modules/{id}` | Modul aktualisieren |
|
||||
| `GET` | `/sdk/v1/training/matrix` | Schulungsmatrix |
|
||||
| `POST` | `/sdk/v1/training/matrix` | Matrix-Eintrag setzen |
|
||||
| `GET` | `/sdk/v1/training/assignments` | Schulungszuweisungen |
|
||||
| `POST` | `/sdk/v1/training/assignments/compute` | Zuweisungen für Nutzer berechnen |
|
||||
| `POST` | `/sdk/v1/training/assignments/{id}/complete` | Schulung abschließen |
|
||||
| `GET` | `/sdk/v1/training/quiz/{moduleId}` | Quiz-Fragen laden |
|
||||
| `POST` | `/sdk/v1/training/quiz/{moduleId}/submit` | Quiz-Antworten einreichen |
|
||||
| `GET` | `/sdk/v1/training/stats` | Schulungsstatistiken |
|
||||
| `GET` | `/sdk/v1/training/deadlines` | Fällige Schulungen |
|
||||
| `POST` | `/sdk/v1/training/escalation/check` | Eskalationen prüfen |
|
||||
| `POST` | `/sdk/v1/training/content/generate` | KI-Content generieren (Ollama) |
|
||||
|
||||
### DB-Tabellen (ai-compliance-sdk PostgreSQL)
|
||||
|
||||
| Tabelle | Modus | Beschreibung |
|
||||
|---------|-------|--------------|
|
||||
| `training_modules` | read/write | Schulungsmodule mit Metadaten |
|
||||
| `training_assignments` | read/write | Mitarbeiterzuweisungen |
|
||||
| `training_quiz_questions` | read/write | Quiz-Fragen je Modul |
|
||||
| `training_quiz_attempts` | read/write | Quiz-Versuche und Ergebnisse |
|
||||
| `training_matrix_entries` | read/write | Rollen-Modul-Zuordnung |
|
||||
| `training_audit_log` | write | Aktivitätsprotokoll |
|
||||
|
||||
### Vordefinierte Module (Auswahl)
|
||||
|
||||
| Code | Titel | Bereich | Typ |
|
||||
|------|-------|---------|-----|
|
||||
| `DSGVO-BASIC` | DSGVO Grundlagen | dsgvo | annual |
|
||||
| `DSGVO-BREACH` | Datenpannen und Meldepflichten | dsgvo | event_trigger |
|
||||
| `DSGVO-DSR` | Betroffenenrechte | dsgvo | annual |
|
||||
| `AI-BAS` | KI-Kompetenz Grundlagen | ai_act | annual |
|
||||
| `AI-RISK` | Hochrisiko-KI-Systeme | ai_act | event_trigger |
|
||||
| `ISMS-AUD` | ISMS Audit-Vorbereitung | iso27001 | event_trigger |
|
||||
| `HIN-BAS` | Hinweisgeberschutz | hinschg | annual |
|
||||
| `PHISH` | Phishing-Erkennung | iso27001 | micro |
|
||||
|
||||
---
|
||||
|
||||
## Datenfluss
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
A[Source Policy] --> B[Requirements]
|
||||
B --> C[Audit Checklist]
|
||||
C --> D[Audit Report]
|
||||
D --> E[Obligations]
|
||||
E --> F[TOMs]
|
||||
F --> G[Löschfristen]
|
||||
G --> H[VVT]
|
||||
|
||||
I[Document Generator] --> J[Workflow]
|
||||
K[Academy] --> L[Training Engine]
|
||||
```
|
||||
|
||||
Die **Source Policy** bildet die Grundlage für alle nachfolgenden Analyse-Schritte.
|
||||
Das **VVT** ist der abschließende Schritt der Dokumentationsphase und baut auf TOMs und Löschfristen auf.
|
||||
Die **Training Engine** operiert parallel im Betrieb-Paket und liefert Evidence für Audits.
|
||||
@@ -66,6 +66,7 @@ nav:
|
||||
- Uebersicht: services/document-crawler/index.md
|
||||
- SDK Module:
|
||||
- Analyse-Module (Paket 2): services/sdk-modules/analyse-module.md
|
||||
- Dokumentations-Module (Paket 3+): services/sdk-modules/dokumentations-module.md
|
||||
- Academy: services/sdk-modules/academy.md
|
||||
- Whistleblower: services/sdk-modules/whistleblower.md
|
||||
- Incidents: services/sdk-modules/incidents.md
|
||||
|
||||
38
scripts/apply_training_migrations.sh
Normal file
38
scripts/apply_training_migrations.sh
Normal file
@@ -0,0 +1,38 @@
|
||||
#!/bin/bash
|
||||
# Apply Training Engine migrations on Mac Mini and verify
|
||||
# Usage: bash scripts/apply_training_migrations.sh
|
||||
|
||||
set -e
|
||||
|
||||
DOCKER="/usr/local/bin/docker"
|
||||
CONTAINER="bp-compliance-ai-sdk"
|
||||
PROJECT_DIR="/Users/benjaminadmin/Projekte/breakpilot-compliance"
|
||||
|
||||
echo "==> Applying Training Engine migrations on Mac Mini..."
|
||||
|
||||
ssh macmini "cd ${PROJECT_DIR} && \
|
||||
${DOCKER} exec ${CONTAINER} \
|
||||
psql \"\${DATABASE_URL}\" -f /migrations/014_training_engine.sql \
|
||||
&& echo 'Migration 014 applied' \
|
||||
|| echo 'Migration 014 may already be applied (table exists)'"
|
||||
|
||||
ssh macmini "cd ${PROJECT_DIR} && \
|
||||
${DOCKER} exec ${CONTAINER} \
|
||||
psql \"\${DATABASE_URL}\" -f /migrations/016_training_media.sql \
|
||||
&& echo 'Migration 016 applied' \
|
||||
|| echo 'Migration 016 may already be applied'"
|
||||
|
||||
echo ""
|
||||
echo "==> Verifying training service..."
|
||||
curl -sf "https://macmini:8093/health" && echo "Health check: OK" || echo "Health check: FAILED"
|
||||
|
||||
echo ""
|
||||
echo "==> Checking training modules endpoint..."
|
||||
curl -sf \
|
||||
"https://macmini:8093/sdk/v1/training/modules" \
|
||||
-H "X-Tenant-ID: 9282a473-5c95-4b3a-bf78-0ecc0ec71d3e" \
|
||||
| python3 -c "import sys,json; d=json.load(sys.stdin); print(f'Modules found: {len(d.get(\"modules\",[]))}')" \
|
||||
|| echo "Training modules endpoint check failed"
|
||||
|
||||
echo ""
|
||||
echo "Done."
|
||||
55
scripts/apply_vvt_migration.sh
Normal file
55
scripts/apply_vvt_migration.sh
Normal file
@@ -0,0 +1,55 @@
|
||||
#!/bin/bash
|
||||
# Apply VVT migration and rebuild backend-compliance on Mac Mini
|
||||
# Usage: bash scripts/apply_vvt_migration.sh
|
||||
|
||||
set -e
|
||||
|
||||
DOCKER="/usr/local/bin/docker"
|
||||
BACKEND_CONTAINER="bp-compliance-backend"
|
||||
PROJECT_DIR="/Users/benjaminadmin/Projekte/breakpilot-compliance"
|
||||
|
||||
echo "==> Pushing code to Mac Mini..."
|
||||
git push origin main && git push gitea main
|
||||
|
||||
echo "==> Pulling code on Mac Mini..."
|
||||
ssh macmini "cd ${PROJECT_DIR} && git pull --no-rebase origin main"
|
||||
|
||||
echo "==> Applying VVT migration (006_vvt.sql)..."
|
||||
ssh macmini "cd ${PROJECT_DIR} && \
|
||||
${DOCKER} exec ${BACKEND_CONTAINER} \
|
||||
python3 -c \"
|
||||
import psycopg2
|
||||
import os
|
||||
|
||||
conn = psycopg2.connect(os.environ['DATABASE_URL'])
|
||||
conn.autocommit = True
|
||||
cur = conn.cursor()
|
||||
with open('/app/migrations/006_vvt.sql', 'r') as f:
|
||||
sql = f.read()
|
||||
cur.execute(sql)
|
||||
cur.close()
|
||||
conn.close()
|
||||
print('VVT migration applied successfully')
|
||||
\"" || echo "Note: Migration may use different DB connection method. Trying psql..."
|
||||
|
||||
ssh macmini "cd ${PROJECT_DIR} && \
|
||||
${DOCKER} exec ${BACKEND_CONTAINER} \
|
||||
psql \"\${DATABASE_URL}\" -f /app/migrations/006_vvt.sql \
|
||||
&& echo 'VVT migration (psql) applied' \
|
||||
|| echo 'Could not apply via psql, check manually'"
|
||||
|
||||
echo ""
|
||||
echo "==> Rebuilding backend-compliance..."
|
||||
ssh macmini "cd ${PROJECT_DIR} && \
|
||||
${DOCKER} compose build --no-cache backend-compliance && \
|
||||
${DOCKER} compose up -d backend-compliance"
|
||||
|
||||
echo ""
|
||||
echo "==> Verifying VVT endpoint..."
|
||||
sleep 5
|
||||
curl -sf "https://macmini:8002/api/compliance/vvt/stats" \
|
||||
| python3 -c "import sys,json; d=json.load(sys.stdin); print(f'VVT stats: total={d.get(\"total\",0)}')" \
|
||||
|| echo "VVT endpoint check: needs backend restart"
|
||||
|
||||
echo ""
|
||||
echo "Done. Check logs: ssh macmini '${DOCKER} logs -f ${BACKEND_CONTAINER}'"
|
||||
Reference in New Issue
Block a user