diff --git a/admin-compliance/app/api/sdk/v1/cra/documents/[docId]/approve/route.ts b/admin-compliance/app/api/sdk/v1/cra/documents/[docId]/approve/route.ts new file mode 100644 index 00000000..3cdb76b1 --- /dev/null +++ b/admin-compliance/app/api/sdk/v1/cra/documents/[docId]/approve/route.ts @@ -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 }) + } +} diff --git a/admin-compliance/app/api/sdk/v1/cra/documents/[docId]/route.ts b/admin-compliance/app/api/sdk/v1/cra/documents/[docId]/route.ts new file mode 100644 index 00000000..f63e974a --- /dev/null +++ b/admin-compliance/app/api/sdk/v1/cra/documents/[docId]/route.ts @@ -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 }) + } +} diff --git a/admin-compliance/app/api/sdk/v1/cra/projects/[id]/documents/generate/route.ts b/admin-compliance/app/api/sdk/v1/cra/projects/[id]/documents/generate/route.ts new file mode 100644 index 00000000..6681c6e8 --- /dev/null +++ b/admin-compliance/app/api/sdk/v1/cra/projects/[id]/documents/generate/route.ts @@ -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 }) + } +} diff --git a/admin-compliance/app/api/sdk/v1/cra/projects/[id]/documents/route.ts b/admin-compliance/app/api/sdk/v1/cra/projects/[id]/documents/route.ts new file mode 100644 index 00000000..6bba90e8 --- /dev/null +++ b/admin-compliance/app/api/sdk/v1/cra/projects/[id]/documents/route.ts @@ -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 }) + } +} diff --git a/admin-compliance/app/sdk/cra/[projectId]/documents/page.tsx b/admin-compliance/app/sdk/cra/[projectId]/documents/page.tsx new file mode 100644 index 00000000..a7de32a0 --- /dev/null +++ b/admin-compliance/app/sdk/cra/[projectId]/documents/page.tsx @@ -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 + 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 = { + 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 = { + 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(null) + const [loading, setLoading] = useState(true) + const [error, setError] = useState('') + const [generating, setGenerating] = useState(null) + const [expanded, setExpanded] = useState(null) + const [docContent, setDocContent] = useState>({}) + + // Generation params per doc type + const [manufacturer, setManufacturer] = useState('') + const [notifiedBody, setNotifiedBody] = useState('') + const [securityContact, setSecurityContact] = useState('') + + // Approval form + const [approving, setApproving] = useState(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 = { 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

Laedt...

+ + return ( +
+
+
+ + ← Zurueck zum Projekt + +

CRA-Dokumente

+

+ DoC (Annex VII), Technische Doku (Annex V), CVD-Policy, Update-Policy, SBOM-Bericht — generiert aus aktuellem Projektstand. +

+
+ + {error && ( +
+
{error}
+ +
+ )} + + {/* Generation params */} +
+ + Optionale Parameter fuer Generierung (Hersteller, NoBo, Security-Contact) + +
+
+ + setManufacturer(e.target.value)} placeholder="Acme GmbH, Musterstr. 1, 80331 Muenchen" className="w-full px-3 py-2 border rounded text-sm" /> +
+
+ + setNotifiedBody(e.target.value)} placeholder="TUEV Nord (NB-0044)" className="w-full px-3 py-2 border rounded text-sm" /> +
+
+ + setSecurityContact(e.target.value)} placeholder="security@example.com" className="w-full px-3 py-2 border rounded text-sm" /> +
+
+
+ +
+ {data?.items.map(doc => ( +
+
+
+
+

{doc.doc_type_label}

+ {doc.version > 0 && ( + v{doc.version} + )} + + {STATUS_LABEL[doc.status]} + +
+ {doc.generated_at && ( +

+ Generiert: {new Date(doc.generated_at).toLocaleString('de-DE')} + {doc.signed_by && doc.signed_at && ( + <> · Freigegeben von {doc.signed_by} am {new Date(doc.signed_at).toLocaleString('de-DE')} + )} +

+ )} + {doc.requirements_coverage && Object.keys(doc.requirements_coverage).length > 0 && ( +

+ Coverage: {String(doc.requirements_coverage.fields_filled || 0)} / {String(doc.requirements_coverage.fields_required || 0)} Pflichtfelder · {String(doc.requirements_coverage.annex_anchor || '')} +

+ )} +
+
+ + {doc.id && ( + + )} +
+
+ + {expanded === doc.id && doc.id && docContent[doc.id] && ( +
+
+

Markdown-Vorschau

+ +
+
+                    {docContent[doc.id]}
+                  
+ + {doc.status === 'draft' && ( +
+

+ Vor Freigabe pruefen ob alle [zu ergaenzen]-Stellen gefuellt sind. +

+
+ setSignedBy(e.target.value)} + placeholder="Name + Rolle des Freigebenden" + className="flex-1 px-2 py-1 border rounded text-sm" + /> + + +
+
+ )} +
+ )} +
+ ))} +
+ +
+ Hinweis: Diese Dokumente sind Skelette aus dem aktuellen Projektstand. Markdown-Format, manuelles Editieren + Unterzeichnung erforderlich vor Inverkehrbringen. PDF-Export folgt in Phase 5.5. +
+
+
+ ) +} diff --git a/backend-compliance/compliance/api/cra_doc_templates.py b/backend-compliance/compliance/api/cra_doc_templates.py new file mode 100644 index 00000000..758ef992 --- /dev/null +++ b/backend-compliance/compliance/api/cra_doc_templates.py @@ -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.0–10.0 | Kritisch | **30 Tage** | +| 7.0–8.9 | Hoch | 90 Tage | +| 4.0–6.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", +} diff --git a/backend-compliance/migrations/122_compliance_cra_documents.sql b/backend-compliance/migrations/122_compliance_cra_documents.sql new file mode 100644 index 00000000..fbc28fda --- /dev/null +++ b/backend-compliance/migrations/122_compliance_cra_documents.sql @@ -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);