feat(cra): Phase 5 — Technical Doc + DoC Generator (Annex V + VII)
CI / detect-changes (push) Successful in 11s
CI / branch-name (push) Has been skipped
CI / secret-scan (push) Has been skipped
CI / dep-audit (push) Has been skipped
CI / sbom-scan (push) Has been skipped
CI / validate-canonical-controls (push) Successful in 15s
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / guardrail-integrity (push) Has been skipped
CI / loc-budget (push) Failing after 16s
CI / go-lint (push) Has been skipped
CI / nodejs-build (push) Successful in 3m1s
CI / test-go (push) Has been skipped
CI / iace-gt-coverage (push) Has been skipped
CI / test-python-backend (push) Successful in 39s
CI / test-python-document-crawler (push) Has been skipped
CI / test-python-dsms-gateway (push) Has been skipped

Migration 122: compliance_cra_documents with versioning + approval workflow
- doc_type whitelist: doc_eu_conformity, doc_technical, doc_cvd_policy,
  doc_update_policy, doc_sbom_report
- Status state machine: draft → reviewed → approved (+ superseded)
- Snapshot generation_context for audit trail

New module cra_doc_templates.py — pure-function generators (no DB access):
- doc_eu_conformity: EU DoC structured per CRA Annex VII (all 7 mandatory fields)
- doc_technical: Technische Dokumentation per CRA Annex V
- doc_cvd_policy: ISO/IEC 29147-compliant CVD policy with SLA table
- doc_update_policy: Patch/Update policy with Lifecycle + CSAF reference
- doc_sbom_report: Latest SBOM summary with top-10 components
Returns (title, markdown_content, requirements_coverage) — coverage tracks
how many mandatory fields are filled vs placeholders.

Backend endpoints:
- POST /documents/generate — generates doc, supersedes previous version,
  increments version number atomically
- GET /documents — lists all 5 doc types (also "not_generated" stubs)
- GET /documents/{id} — full content_md
- POST /documents/{id}/approve — set status + signed_by + signed_at

Frontend:
- /documents page: 5 doc-type cards with Generate/Re-Generate buttons,
  inline Markdown preview with .md download, 2-step approval flow
  (reviewed → approved with signature)
- Optional params form: manufacturer, notified_body, security_contact
- Dashboard: +1 button (Dokumente, 7 buttons total)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Benjamin Admin
2026-05-18 22:10:23 +02:00
parent cc80e59e5e
commit 27384aea09
7 changed files with 887 additions and 0 deletions
@@ -0,0 +1,23 @@
import { NextRequest, NextResponse } from 'next/server'
const BACKEND_URL = process.env.BACKEND_URL || 'http://backend-compliance:8002'
export async function POST(request: NextRequest, ctx: { params: Promise<{ docId: string }> }) {
const { docId } = await ctx.params
const tenantId = request.headers.get('x-tenant-id') || '00000000-0000-0000-0000-000000000001'
const body = await request.text()
try {
const resp = await fetch(`${BACKEND_URL}/api/v1/cra/projects/documents/${docId}/approve`, {
method: 'POST',
headers: { 'X-Tenant-ID': tenantId, 'Content-Type': 'application/json' },
body,
})
const text = await resp.text()
return new NextResponse(text, {
status: resp.status,
headers: { 'Content-Type': resp.headers.get('Content-Type') || 'application/json' },
})
} catch (err) {
return NextResponse.json({ error: 'Backend unreachable', details: String(err) }, { status: 502 })
}
}
@@ -0,0 +1,23 @@
import { NextRequest, NextResponse } from 'next/server'
const BACKEND_URL = process.env.BACKEND_URL || 'http://backend-compliance:8002'
function tenant(req: NextRequest) {
return req.headers.get('x-tenant-id') || '00000000-0000-0000-0000-000000000001'
}
export async function GET(request: NextRequest, ctx: { params: Promise<{ docId: string }> }) {
const { docId } = await ctx.params
try {
const resp = await fetch(`${BACKEND_URL}/api/v1/cra/projects/documents/${docId}`, {
headers: { 'X-Tenant-ID': tenant(request) },
})
const text = await resp.text()
return new NextResponse(text, {
status: resp.status,
headers: { 'Content-Type': resp.headers.get('Content-Type') || 'application/json' },
})
} catch (err) {
return NextResponse.json({ error: 'Backend unreachable', details: String(err) }, { status: 502 })
}
}
@@ -0,0 +1,23 @@
import { NextRequest, NextResponse } from 'next/server'
const BACKEND_URL = process.env.BACKEND_URL || 'http://backend-compliance:8002'
export async function POST(request: NextRequest, ctx: { params: Promise<{ id: string }> }) {
const { id } = await ctx.params
const tenantId = request.headers.get('x-tenant-id') || '00000000-0000-0000-0000-000000000001'
const body = await request.text()
try {
const resp = await fetch(`${BACKEND_URL}/api/v1/cra/projects/${id}/documents/generate`, {
method: 'POST',
headers: { 'X-Tenant-ID': tenantId, 'Content-Type': 'application/json' },
body,
})
const text = await resp.text()
return new NextResponse(text, {
status: resp.status,
headers: { 'Content-Type': resp.headers.get('Content-Type') || 'application/json' },
})
} catch (err) {
return NextResponse.json({ error: 'Backend unreachable', details: String(err) }, { status: 502 })
}
}
@@ -0,0 +1,26 @@
import { NextRequest, NextResponse } from 'next/server'
const BACKEND_URL = process.env.BACKEND_URL || 'http://backend-compliance:8002'
function tenant(req: NextRequest) {
return req.headers.get('x-tenant-id') || '00000000-0000-0000-0000-000000000001'
}
export async function GET(request: NextRequest, ctx: { params: Promise<{ id: string }> }) {
const { id } = await ctx.params
const { searchParams } = new URL(request.url)
const qs = searchParams.toString()
try {
const resp = await fetch(
`${BACKEND_URL}/api/v1/cra/projects/${id}/documents${qs ? `?${qs}` : ''}`,
{ headers: { 'X-Tenant-ID': tenant(request) } }
)
const text = await resp.text()
return new NextResponse(text, {
status: resp.status,
headers: { 'Content-Type': resp.headers.get('Content-Type') || 'application/json' },
})
} catch (err) {
return NextResponse.json({ error: 'Backend unreachable', details: String(err) }, { status: 502 })
}
}
@@ -0,0 +1,309 @@
'use client'
import React, { useEffect, useState, useCallback, use } from 'react'
interface DocItem {
id: string | null
doc_type: string
doc_type_label: string
title: string
content_md: string | null
version: number
requirements_coverage: Record<string, unknown>
status: string
signed_by: string | null
signed_at: string | null
generated_at: string | null
superseded_at: string | null
}
interface DocListResponse {
project_id: string
total: number
items: DocItem[]
}
const STATUS_STYLE: Record<string, string> = {
draft: 'bg-yellow-100 text-yellow-800',
reviewed: 'bg-blue-100 text-blue-800',
approved: 'bg-green-100 text-green-800',
superseded: 'bg-gray-200 text-gray-600',
not_generated: 'bg-gray-100 text-gray-400',
}
const STATUS_LABEL: Record<string, string> = {
draft: 'Entwurf',
reviewed: 'Geprueft',
approved: 'Freigegeben',
superseded: 'Veraltet',
not_generated: 'Nicht erzeugt',
}
export default function DocumentsPage({
params,
}: {
params: Promise<{ projectId: string }>
}) {
const { projectId } = use(params)
const [data, setData] = useState<DocListResponse | null>(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState('')
const [generating, setGenerating] = useState<string | null>(null)
const [expanded, setExpanded] = useState<string | null>(null)
const [docContent, setDocContent] = useState<Record<string, string>>({})
// Generation params per doc type
const [manufacturer, setManufacturer] = useState('')
const [notifiedBody, setNotifiedBody] = useState('')
const [securityContact, setSecurityContact] = useState('')
// Approval form
const [approving, setApproving] = useState<string | null>(null)
const [signedBy, setSignedBy] = useState('')
const tenant = '00000000-0000-0000-0000-000000000001'
const load = useCallback(async () => {
try {
const res = await fetch(`/api/sdk/v1/cra/projects/${projectId}/documents`, {
headers: { 'X-Tenant-ID': tenant },
})
if (!res.ok) throw new Error(await res.text())
setData(await res.json())
} catch (e) {
setError(e instanceof Error ? e.message : 'Fehler beim Laden')
} finally {
setLoading(false)
}
}, [projectId])
useEffect(() => { load() }, [load])
const generate = async (docType: string) => {
setGenerating(docType)
setError('')
try {
const body: Record<string, string> = { doc_type: docType }
if (docType === 'doc_eu_conformity') {
if (manufacturer) body.manufacturer = manufacturer
if (notifiedBody) body.notified_body = notifiedBody
}
if (docType === 'doc_cvd_policy' && securityContact) {
body.security_contact = securityContact
}
const res = await fetch(`/api/sdk/v1/cra/projects/${projectId}/documents/generate`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-Tenant-ID': tenant },
body: JSON.stringify(body),
})
if (!res.ok) throw new Error(await res.text())
const doc = await res.json()
setDocContent(prev => ({ ...prev, [doc.id]: doc.content_md }))
await load()
} catch (e) {
setError(e instanceof Error ? e.message : 'Generierung fehlgeschlagen')
} finally {
setGenerating(null)
}
}
const loadContent = async (docId: string) => {
if (docContent[docId]) {
setExpanded(expanded === docId ? null : docId)
return
}
try {
const res = await fetch(`/api/sdk/v1/cra/documents/${docId}`, {
headers: { 'X-Tenant-ID': tenant },
})
if (!res.ok) throw new Error(await res.text())
const doc = await res.json()
setDocContent(prev => ({ ...prev, [docId]: doc.content_md }))
setExpanded(docId)
} catch (e) {
setError(e instanceof Error ? e.message : 'Laden fehlgeschlagen')
}
}
const approve = async (docId: string, status: string) => {
if (!signedBy.trim()) {
setError('Bitte Namen zur Freigabe eintragen.')
return
}
setApproving(docId)
setError('')
try {
const res = await fetch(`/api/sdk/v1/cra/documents/${docId}/approve`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-Tenant-ID': tenant },
body: JSON.stringify({ signed_by: signedBy, status }),
})
if (!res.ok) throw new Error(await res.text())
await load()
} catch (e) {
setError(e instanceof Error ? e.message : 'Freigabe fehlgeschlagen')
} finally {
setApproving(null)
}
}
const download = (doc: DocItem) => {
const content = docContent[doc.id || ''] || doc.content_md || ''
if (!content) return
const blob = new Blob([content], { type: 'text/markdown;charset=utf-8' })
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = `${doc.doc_type}_v${doc.version}_${doc.id?.slice(0, 8)}.md`
a.click()
URL.revokeObjectURL(url)
}
if (loading) return <div className="min-h-screen bg-gray-50 p-8"><p className="text-gray-500">Laedt...</p></div>
return (
<div className="min-h-screen bg-gray-50 py-8">
<div className="max-w-5xl mx-auto px-4">
<div className="mb-6">
<a href={`/sdk/cra/${projectId}`} className="text-sm text-blue-600 hover:underline">
&larr; Zurueck zum Projekt
</a>
<h1 className="text-2xl font-bold text-gray-900 mt-2">CRA-Dokumente</h1>
<p className="text-sm text-gray-600 mt-1">
DoC (Annex VII), Technische Doku (Annex V), CVD-Policy, Update-Policy, SBOM-Bericht generiert aus aktuellem Projektstand.
</p>
</div>
{error && (
<div className="mb-4 bg-red-50 border border-red-200 rounded-lg p-3 text-sm text-red-700">
<pre className="whitespace-pre-wrap">{error}</pre>
<button onClick={() => setError('')} className="text-xs underline mt-1">Schliessen</button>
</div>
)}
{/* Generation params */}
<details className="bg-white rounded-xl border border-gray-200 p-4 mb-4">
<summary className="cursor-pointer text-sm font-medium text-gray-700">
Optionale Parameter fuer Generierung (Hersteller, NoBo, Security-Contact)
</summary>
<div className="mt-3 space-y-3">
<div>
<label className="block text-xs text-gray-600 mb-1">Hersteller (fuer DoC)</label>
<input value={manufacturer} onChange={e => setManufacturer(e.target.value)} placeholder="Acme GmbH, Musterstr. 1, 80331 Muenchen" className="w-full px-3 py-2 border rounded text-sm" />
</div>
<div>
<label className="block text-xs text-gray-600 mb-1">Notified Body (falls Modul C)</label>
<input value={notifiedBody} onChange={e => setNotifiedBody(e.target.value)} placeholder="TUEV Nord (NB-0044)" className="w-full px-3 py-2 border rounded text-sm" />
</div>
<div>
<label className="block text-xs text-gray-600 mb-1">Security-Contact (fuer CVD-Policy)</label>
<input type="email" value={securityContact} onChange={e => setSecurityContact(e.target.value)} placeholder="security@example.com" className="w-full px-3 py-2 border rounded text-sm" />
</div>
</div>
</details>
<div className="space-y-3">
{data?.items.map(doc => (
<div key={doc.doc_type} className="bg-white rounded-xl shadow-sm border border-gray-200 p-5">
<div className="flex items-start justify-between gap-4 mb-3">
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 flex-wrap">
<h3 className="font-semibold text-gray-900">{doc.doc_type_label}</h3>
{doc.version > 0 && (
<span className="text-xs text-gray-500">v{doc.version}</span>
)}
<span className={`px-2 py-0.5 text-xs rounded ${STATUS_STYLE[doc.status]}`}>
{STATUS_LABEL[doc.status]}
</span>
</div>
{doc.generated_at && (
<p className="text-xs text-gray-500 mt-1">
Generiert: {new Date(doc.generated_at).toLocaleString('de-DE')}
{doc.signed_by && doc.signed_at && (
<> · Freigegeben von <span className="font-medium">{doc.signed_by}</span> am {new Date(doc.signed_at).toLocaleString('de-DE')}</>
)}
</p>
)}
{doc.requirements_coverage && Object.keys(doc.requirements_coverage).length > 0 && (
<p className="text-xs text-gray-500 mt-1">
Coverage: {String(doc.requirements_coverage.fields_filled || 0)} / {String(doc.requirements_coverage.fields_required || 0)} Pflichtfelder · {String(doc.requirements_coverage.annex_anchor || '')}
</p>
)}
</div>
<div className="flex gap-2 flex-shrink-0">
<button
onClick={() => generate(doc.doc_type)}
disabled={generating === doc.doc_type}
className="px-3 py-1.5 bg-red-600 text-white text-sm rounded hover:bg-red-700 disabled:bg-gray-300"
>
{generating === doc.doc_type ? 'Generiere...' : (doc.version === 0 ? 'Generieren' : 'Neu generieren')}
</button>
{doc.id && (
<button
onClick={() => loadContent(doc.id!)}
className="px-3 py-1.5 bg-gray-100 text-gray-700 text-sm rounded hover:bg-gray-200"
>
{expanded === doc.id ? 'Einklappen' : 'Inhalt'}
</button>
)}
</div>
</div>
{expanded === doc.id && doc.id && docContent[doc.id] && (
<div className="mt-3 border-t border-gray-200 pt-3">
<div className="flex items-center justify-between mb-2">
<p className="text-xs text-gray-500 font-mono">Markdown-Vorschau</p>
<button
onClick={() => download(doc)}
className="text-xs px-2 py-1 bg-blue-100 text-blue-700 rounded hover:bg-blue-200"
>
Download (.md)
</button>
</div>
<pre className="bg-gray-50 rounded p-3 text-xs overflow-x-auto max-h-96 whitespace-pre-wrap font-mono">
{docContent[doc.id]}
</pre>
{doc.status === 'draft' && (
<div className="mt-3 p-3 bg-yellow-50 border border-yellow-200 rounded">
<p className="text-xs text-yellow-800 mb-2">
Vor Freigabe pruefen ob alle <code>[zu ergaenzen]</code>-Stellen gefuellt sind.
</p>
<div className="flex items-center gap-2">
<input
type="text"
value={signedBy}
onChange={e => setSignedBy(e.target.value)}
placeholder="Name + Rolle des Freigebenden"
className="flex-1 px-2 py-1 border rounded text-sm"
/>
<button
onClick={() => approve(doc.id!, 'reviewed')}
disabled={approving === doc.id || !signedBy.trim()}
className="px-3 py-1 bg-blue-600 text-white text-xs rounded hover:bg-blue-700 disabled:bg-gray-300"
>
Als geprueft markieren
</button>
<button
onClick={() => approve(doc.id!, 'approved')}
disabled={approving === doc.id || !signedBy.trim()}
className="px-3 py-1 bg-green-600 text-white text-xs rounded hover:bg-green-700 disabled:bg-gray-300"
>
Freigeben
</button>
</div>
</div>
)}
</div>
)}
</div>
))}
</div>
<div className="mt-6 bg-blue-50 border border-blue-200 rounded-xl p-4 text-sm text-blue-900">
<strong>Hinweis:</strong> Diese Dokumente sind <em>Skelette</em> aus dem aktuellen Projektstand. Markdown-Format, manuelles Editieren + Unterzeichnung erforderlich vor Inverkehrbringen. PDF-Export folgt in Phase 5.5.
</div>
</div>
</div>
)
}
@@ -0,0 +1,444 @@
"""
CRA Document Templates generates Markdown documents from project state.
Each template takes a project dict + optional context (sboms, vulnerabilities,
checks) and returns (title, markdown_content). Pure functions, no DB access.
Templates implement:
- doc_eu_conformity EU Declaration of Conformity (CRA Annex VII)
- doc_technical Technische Dokumentation (CRA Annex V)
- doc_cvd_policy Coordinated Vulnerability Disclosure Policy
- doc_update_policy Update / Patch Policy
- doc_sbom_report SBOM-Zusammenfassung
"""
from datetime import datetime
from typing import Optional
PATH_LABELS = {
"self_assessment": "Modul A — Self-Assessment durch Hersteller",
"harmonized_standard": "Modul B — Harmonisierte Norm",
"eucc": "Modul H — EUCC-Zertifizierung",
"notified_body": "Modul C — Konformitaetsbewertung durch Notified Body",
}
CLASS_LABELS = {
"NOT_IN_SCOPE": "Nicht im CRA-Scope",
"STANDARD": "Standard-Produkt mit digitalen Elementen",
"IMPORTANT_I": "Wichtiges Produkt — Annex III Klasse I",
"IMPORTANT_II": "Wichtiges Produkt — Annex III Klasse II",
"CRITICAL": "Kritisches Produkt — Annex IV",
}
def _today() -> str:
return datetime.utcnow().strftime("%Y-%m-%d")
def _fmt_or_placeholder(v: Optional[str], placeholder: str = "[zu ergaenzen]") -> str:
return v if v else placeholder
def doc_eu_conformity(project: dict, *, manufacturer: Optional[str] = None,
notified_body: Optional[str] = None) -> tuple[str, str]:
"""EU Declaration of Conformity nach CRA Annex VII.
Pflichtfelder gemaess CRA Art. 28 + Annex VII:
1. Produkt + Modell + eindeutige Kennung
2. Hersteller + Anschrift
3. (Optional) Bevollmaechtigter
4. Konformitaetserklaerung mit Bezug auf CRA
5. Verweis auf angewandte harmonisierte Normen
6. Notified Body (falls Modul C)
7. Datum + Ort + Unterschrift
"""
name = project.get("name", "[zu ergaenzen]")
classification = project.get("cra_classification", "unbewertet")
path = project.get("conformity_path", "self_assessment")
path_label = PATH_LABELS.get(path, path)
class_label = CLASS_LABELS.get(classification, classification)
desc = project.get("description") or project.get("intended_use") or ""
title = f"EU-Konformitaetserklaerung — {name}"
content = f"""# EU-Konformitaetserklaerung (DoC)
**Gemaess Verordnung (EU) 2024/2847 Cyber Resilience Act, Anhang VII**
---
## 1. Eindeutige Kennung des Produkts
**Produktname:** {name}
**Modell / Typ:** {_fmt_or_placeholder(project.get('primary_language'), 'Software-Produkt')}
**Repository / Source:** {_fmt_or_placeholder(project.get('repo_url'), '[Repo-URL ergaenzen]')}
**Beschreibung:** {desc or '[Produktbeschreibung ergaenzen]'}
## 2. Hersteller
**Name:** {_fmt_or_placeholder(manufacturer, '[Hersteller-Name + Adresse ergaenzen]')}
**Anschrift:** [zu ergaenzen]
**Kontakt:** [zu ergaenzen]
## 3. Bevollmaechtigter (Authorised Representative)
[Falls Hersteller nicht in EU ansaessig sonst entfallen]
## 4. Konformitaetserklaerung
Die alleinige Verantwortung fuer die Ausstellung dieser Konformitaetserklaerung
traegt der Hersteller. Der Gegenstand der oben beschriebenen Erklaerung
**erfuellt die einschlaegigen Harmonisierungsrechtsvorschriften der Union**:
- **Verordnung (EU) 2024/2847** (Cyber Resilience Act) vollstaendig
einschliesslich der **wesentlichen Cybersicherheitsanforderungen nach
Anhang I**.
**CRA-Klassifizierung:** {class_label}
**Angewandtes Konformitaetsbewertungsverfahren:** {path_label}
## 5. Verweise auf angewandte harmonisierte Normen und technische Spezifikationen
- Harmonisierte Norm: DIN EN 40000-1-2 (Entwurf, Stand 11/2025)
- ETSI EN 303 645 (Consumer-IoT-Cybersicherheit)
- ISO/IEC 29147 (Vulnerability Disclosure)
- ISO/IEC 30111 (Vulnerability Handling Processes)
- ENISA Technical Implementation Guidance (NIS2-Bezug)
## 6. Notified Body
{'_' if not notified_body else notified_body}
{'Identifikationsnummer: [zu ergaenzen]' if notified_body else '(keine — Self-Assessment durch Hersteller)'}
## 7. Zusatzangaben
Diese Konformitaetserklaerung wurde unterzeichnet im Namen von:
**Ort, Datum:** [zu ergaenzen], {_today()}
**Name, Funktion:** [zu ergaenzen]
**Unterschrift:** ___________________________
---
*Diese Erklaerung wurde automatisch aus dem BreakPilot CRA-Modul generiert.
Pflichtangaben sind als `[zu ergaenzen]` markiert und muessen vor Inverkehrbringen
ausgefuellt + manuell unterzeichnet werden.*
"""
coverage = {
"doc_type": "doc_eu_conformity",
"annex_anchor": "CRA Anhang VII",
"fields_required": 7,
"fields_filled": sum(1 for v in [name, classification, path, desc] if v and not str(v).startswith("[")),
}
return title, content, coverage
def doc_technical(project: dict) -> tuple[str, str, dict]:
"""Technische Dokumentation nach CRA Annex V."""
name = project.get("name", "[Produktname]")
desc = project.get("description") or project.get("intended_use") or ""
title = f"Technische Dokumentation — {name}"
content = f"""# Technische Dokumentation
**{name}**
Gemaess Verordnung (EU) 2024/2847 (CRA), Anhang V
Generiert am: {_today()}
---
## 1. Allgemeine Beschreibung des Produkts
**Produktname:** {name}
**Verwendungszweck:** {_fmt_or_placeholder(project.get('intended_use'))}
**Hauptfunktionen:** {desc or '[ergaenzen]'}
### 1.1 Technische Eigenschaften
- **Primaere Programmiersprache:** {_fmt_or_placeholder(project.get('primary_language'))}
- **Enthaelt Firmware:** {'Ja' if project.get('has_firmware') else 'Nein'}
- **Internet-Konnektivitaet:** {'Ja' if project.get('connected_to_internet') else 'Nein'}
- **Software-Updates:** {'Ja' if project.get('has_software_updates') else 'Nein'}
- **Verarbeitet personenbezogene Daten:** {'Ja' if project.get('processes_personal_data') else 'Nein'}
- **Kritische Infrastruktur:** {'Ja' if project.get('is_critical_infra_supplier') else 'Nein'}
## 2. Konzeption, Entwicklung und Produktion
### 2.1 Cybersicherheits-Risikobewertung (CRA Art. 13(2))
Risikobewertung wurde durchgefuehrt fuer die Klassifikation
**{CLASS_LABELS.get(project.get('cra_classification', ''), 'unbewertet')}**.
### 2.2 Secure-by-Design-Massnahmen
Siehe `doc_eu_conformity` Abschnitt 4 fuer die Einhaltung von Anhang I.
Detail-Mapping (40 atomare Annex-I-Anforderungen) ist im BreakPilot
CRA-Modul unter `/sdk/cra/{{projectId}}/requirements` einsehbar.
## 3. Bewertung der Konformitaet
### 3.1 Verfahren
{PATH_LABELS.get(project.get('conformity_path', ''), 'Nicht festgelegt')}
### 3.2 Klassifikation
**{CLASS_LABELS.get(project.get('cra_classification', ''), 'unbewertet')}**
Begruendung:
{chr(10).join(f'- {r}' for r in project.get('classification_rationale', []) or ['(keine Rationale erfasst)'])}
## 4. Kontaktangaben
**Hersteller:** [zu ergaenzen]
**Anschrift:** [zu ergaenzen]
**Cybersicherheits-Kontakt:** [zu ergaenzen, siehe CVD-Policy]
---
*Diese Datei wurde automatisch aus dem BreakPilot CRA-Modul generiert.
Sie ist ein Skelett Hersteller muss alle `[ergaenzen]`-Stellen fuellen,
zusaetzliche Anhaenge (Architekturzeichnungen, Test-Berichte, SBOM) beilegen.*
"""
coverage = {
"doc_type": "doc_technical",
"annex_anchor": "CRA Anhang V",
"fields_required": 8,
"fields_filled": sum(1 for v in [
project.get("name"), project.get("intended_use"),
project.get("cra_classification"), project.get("conformity_path"),
] if v),
}
return title, content, coverage
def doc_cvd_policy(project: dict, *, security_contact: Optional[str] = None) -> tuple[str, str, dict]:
"""Coordinated Vulnerability Disclosure Policy."""
name = project.get("name", "[Produkt]")
contact = _fmt_or_placeholder(security_contact, "security@[your-domain]")
title = f"Coordinated Vulnerability Disclosure Policy — {name}"
content = f"""# Coordinated Vulnerability Disclosure (CVD) Policy
**{name}**
Generiert am: {_today()}
Diese Policy beschreibt, wie Sicherheitsforscher und Nutzer Schwachstellen
verantwortungsvoll an uns melden koennen.
---
## Wir freuen uns ueber Schwachstellen-Meldungen
Wenn Sie eine Sicherheitsluecke in **{name}** entdeckt haben, melden Sie sie
bitte vertraulich an unsere Sicherheitsstelle. Wir verpflichten uns:
- **Eingangsbestaetigung innerhalb von 5 Werktagen**
- Kommunikation auf Augenhoehe waehrend der Triage
- Koordinierte Veroeffentlichung nach Patch-Verfuegbarkeit
- Public Acknowledgement (auf Wunsch)
- Kein juristisches Vorgehen gegen Forscher, die diese Policy befolgen
## Meldekanal
- **E-Mail:** {contact}
- **PGP-Key:** [Fingerprint einfuegen]
- **Web:** https://[your-domain]/.well-known/security.txt
- **Sprachen:** Deutsch, English
## Was wir brauchen
- Eindeutige Produkt-Identifikation: {name} + Version
- Reproduktionsschritte (PoC bevorzugt)
- Auswirkungsbeschreibung (Confidentiality / Integrity / Availability)
- (Optional) Vorschlag fuer Behebung
## Reaktionszeit-Ziele (SLAs)
| Severity (CVSS) | Triage | Patch verfuegbar |
|---|---|---|
| Kritisch (9.0+) | 24h | 30 Tage |
| Hoch (7.0-8.9) | 72h | 90 Tage |
| Mittel (4.0-6.9) | 7 Tage | 180 Tage |
| Niedrig (<4.0) | 14 Tage | naechster Release-Zyklus |
## Embargo + Disclosure
Wir bitten Sicherheitsforscher um **Embargo bis Patch ausgeliefert** (typ. 90 Tage).
Nach Patch koordinieren wir die Veroeffentlichung gemeinsam (Forscher +
Hersteller). CVE-Beantragung erfolgt durch uns.
## CRA-Bezug
Diese Policy ist Pflicht gemaess **CRA Anhang I Teil 2(5)** sowie
**ISO/IEC 29147**. Bei aktiv ausgenutzten Schwachstellen melden wir nach
**CRA Art. 14(2)**:
- **24h:** Fruehwarnung an ENISA / nationale CSIRT
- **72h:** Detaillierter Bericht
- **14 Tage:** Abschlussbericht nach Behebung
---
*Diese CVD-Policy wurde automatisch generiert. Hersteller-spezifische Angaben
(Kontakt, PGP, Domain) muessen ergaenzt werden vor Veroeffentlichung.*
"""
coverage = {
"doc_type": "doc_cvd_policy",
"annex_anchor": "CRA Anhang I Teil 2(5), Art. 14",
"fields_required": 3,
"fields_filled": 1 if security_contact else 0,
}
return title, content, coverage
def doc_update_policy(project: dict) -> tuple[str, str, dict]:
"""Update / Patch Policy."""
name = project.get("name", "[Produkt]")
title = f"Update- und Patch-Policy — {name}"
content = f"""# Update- und Patch-Policy
**{name}**
Generiert am: {_today()}
---
## Lifecycle-Support
Wir verpflichten uns, **Sicherheits-Updates fuer {name}** waehrend der
gesamten erwarteten Nutzungsdauer bereitzustellen, jedoch **mindestens 5 Jahre**
ab Inverkehrbringen gemaess CRA Anhang I, 1(4).
## Patch-SLA (Severity-basiert)
| CVSS Score | Severity | Max. Patch-Zeit |
|---|---|---|
| 9.010.0 | Kritisch | **30 Tage** |
| 7.08.9 | Hoch | 90 Tage |
| 4.06.9 | Mittel | 180 Tage |
| <4.0 | Niedrig | naechster Release |
## Update-Mechanismus
- **Distribution:** Ueber sichere HTTPS-Kanaele
- **Authentizitaet:** Alle Updates sind digital signiert (X.509)
- **Integritaet:** SHA-256-Hash vor Installation geprueft
- **Rollback:** Manuelles Rollback auf vorherige Version moeglich
- **Manipulationsschutz:** Downgrade auf verwundbare Versionen blockiert
## Benachrichtigung
Bei kritischen Updates: aktive Benachrichtigung der Endnutzer ueber
{_fmt_or_placeholder(project.get('repo_url'), '[Kommunikationskanal ergaenzen]')}
binnen 24h nach Verfuegbarkeit.
## Security Advisories
Veroeffentlicht im CSAF-Format (Common Security Advisory Framework) unter:
- https://[your-domain]/security/advisories
- Maschinenlesbar fuer SCA-Tools
## End-of-Life
Mindestens **6 Monate vor EOL** wird der Termin bekannt gegeben. Letztes
Sicherheits-Update wird mit der EOL-Ankuendigung ausgeliefert.
## CRA-Bezug
Diese Policy erfuellt CRA Anhang I, 1(4) (sichere Updates) und
2(3) (Patch-Bereitstellung), sowie Art. 13 (Lifecycle-Support).
---
*Hersteller-spezifische Angaben muessen ergaenzt werden.*
"""
coverage = {
"doc_type": "doc_update_policy",
"annex_anchor": "CRA Anhang I 1(4) + 2(3), Art. 13",
"fields_required": 4,
"fields_filled": 1 if project.get("has_software_updates") else 0,
}
return title, content, coverage
def doc_sbom_report(project: dict, latest_sbom: Optional[dict] = None) -> tuple[str, str, dict]:
"""SBOM-Zusammenfassung des aktuellsten Uploads."""
name = project.get("name", "[Produkt]")
title = f"SBOM-Bericht — {name}"
if not latest_sbom:
body = """## Kein SBOM hochgeladen
Bitte SBOM (CycloneDX oder SPDX) hochladen unter:
`/sdk/cra/{projectId}/sbom`
CRA Anhang I, Anforderung 23 verlangt ein maschinenlesbares
Software Bill of Materials fuer jedes Produkt mit digitalen Elementen.
"""
fields_filled = 0
else:
summary = latest_sbom.get("summary") or {}
sample = summary.get("sample_components") or summary.get("sample_packages") or []
sample_list = "\n".join(f"- `{c.get('name', '?')}` v{c.get('version', '?')}" for c in sample[:10])
body = f"""## Aktuelle Version
- **Datei:** {latest_sbom.get('filename', 'unknown')}
- **Format:** {latest_sbom.get('format', '?')} v{latest_sbom.get('spec_version', '?')}
- **Hochgeladen:** {latest_sbom.get('uploaded_at', '?')}
- **Komponenten:** {latest_sbom.get('component_count', 0)}
- **Scan-Status:** {latest_sbom.get('scan_status', '?')}
## Beispielkomponenten (Top 10)
{sample_list or '_(keine extrahiert)_'}
## Schwachstellen-Status
{'Scan ausstehend — wird durch separates Tool durchgefuehrt.' if latest_sbom.get('scan_status') == 'pending' else 'Siehe `/sdk/cra/{projectId}/vuln` fuer aktive Vulnerabilities.'}
"""
fields_filled = 4
content = f"""# SBOM-Bericht
**{name}**
Generiert am: {_today()}
{body}
---
## CRA-Bezug
Anhang I, Anforderung 23: maschinenlesbares SBOM (CycloneDX oder SPDX)
fuer jedes Produkt mit digitalen Elementen. Pflicht bei jedem Release zu
aktualisieren.
## Naechste Schritte
1. SBOM bei jedem Release-Update neu hochladen
2. osv.dev-Scan pruefen (Phase 3.5)
3. CVEs in `affected_components` der `/vuln`-Seite tracken
"""
coverage = {
"doc_type": "doc_sbom_report",
"annex_anchor": "CRA Anhang I, Req. 23",
"fields_required": 4,
"fields_filled": fields_filled,
}
return title, content, coverage
# =============================================================================
# Registry — for the /documents/generate endpoint
# =============================================================================
DOC_GENERATORS = {
"doc_eu_conformity": doc_eu_conformity,
"doc_technical": doc_technical,
"doc_cvd_policy": doc_cvd_policy,
"doc_update_policy": doc_update_policy,
"doc_sbom_report": doc_sbom_report,
}
DOC_TYPE_LABELS = {
"doc_eu_conformity": "EU-Konformitaetserklaerung (Annex VII)",
"doc_technical": "Technische Dokumentation (Annex V)",
"doc_cvd_policy": "CVD Policy",
"doc_update_policy": "Update- und Patch-Policy",
"doc_sbom_report": "SBOM-Bericht",
}
@@ -0,0 +1,39 @@
-- Migration 122: CRA Generated Documents
-- Tracks all auto-generated CRA documents (DoC, Technical Doc, CVD Policy, etc.)
-- with versioning so customers can audit-trail changes over time.
CREATE TABLE IF NOT EXISTS compliance_cra_documents (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
cra_project_id UUID NOT NULL,
tenant_id VARCHAR(255) NOT NULL,
-- Document classification
doc_type VARCHAR(50) NOT NULL,
-- doc_eu_conformity -- EU DoC (Annex VII)
-- doc_technical -- Technische Doku (Annex V)
-- doc_cvd_policy -- Coordinated Vulnerability Disclosure Policy
-- doc_update_policy -- Update / Patch Policy
-- doc_sbom_report -- SBOM-Zusammenfassung
-- Content (Markdown, can be converted to PDF later)
title VARCHAR(500) NOT NULL,
content_md TEXT NOT NULL,
version INTEGER NOT NULL DEFAULT 1,
-- Compliance metadata
requirements_coverage JSONB DEFAULT '{}'::jsonb, -- e.g. {covered: ["CRA-AI-23", ...], total: 40}
generation_context JSONB DEFAULT '{}'::jsonb, -- snapshot of project state at generation
-- Signature/Approval (Phase 5 minimum: just status)
status VARCHAR(20) NOT NULL DEFAULT 'draft', -- draft | reviewed | approved | superseded
signed_by VARCHAR(255),
signed_at TIMESTAMPTZ,
generated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
superseded_at TIMESTAMPTZ
);
CREATE INDEX IF NOT EXISTS idx_cra_docs_project ON compliance_cra_documents(cra_project_id);
CREATE INDEX IF NOT EXISTS idx_cra_docs_tenant ON compliance_cra_documents(tenant_id);
CREATE INDEX IF NOT EXISTS idx_cra_docs_type ON compliance_cra_documents(cra_project_id, doc_type, generated_at DESC);
CREATE INDEX IF NOT EXISTS idx_cra_docs_status ON compliance_cra_documents(cra_project_id, status);