Files
breakpilot-compliance/admin-compliance/app/sdk/cra-meldewesen/_hooks/useMeldewesen.ts
T
Benjamin Bönisch 8f21650d74
CI / detect-changes (push) Successful in 16s
CI / branch-name (push) Has been skipped
CI / guardrail-integrity (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 / build-sha-integrity (push) Successful in 15s
CI / validate-canonical-controls (push) Successful in 13s
CI / loc-budget (push) Successful in 25s
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / nodejs-build (push) Successful in 3m9s
CI / test-go (push) Has been skipped
CI / iace-gt-coverage (push) Has been skipped
CI / test-python-backend (push) Successful in 31s
CI / test-python-document-crawler (push) Has been skipped
CI / test-python-dsms-gateway (push) Has been skipped
feat(sdk): Kunden-Dokumente + CRA-Meldewesen, Screening aus Frontend genommen
- /sdk/dokumente: Kundensicht nur auf veroeffentlichte Rechtsdokumente
  (Ansehen + Download); Proxy mit Allow-List nur /public — Templates/Drafts/
  Generator bleiben unerreichbar.
- /sdk/cra-meldewesen: CRA Art. 14 Meldewesen (24h/72h/14d-Kaskade) mit
  Fristen-Tracking + ENISA-SRP-Export-Entwurf (kein Live-API). Backend:
  cra_meldewesen (pure, getestet) + cra_incident_store (schema-neutral ueber
  compliance_cra_documents) + /api/v1/cra/incidents (additiv, contract-safe).
- Screening (Self-Scan) aus dem Frontend genommen: Flow-Stepper-Eintrag
  ausgeblendet (visibleWhen), Dashboard-Kachel + Import-Button entfernt.
  Repo-Scanning laeuft extern im Compliance-Scanner; Backend-Router bleibt
  vorerst gemountet (Contract-Stabilitaet).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-17 21:21:28 +02:00

127 lines
4.5 KiB
TypeScript

'use client'
import { useCallback, useEffect, useState } from 'react'
// CRA Art. 14 Meldewesen: incident reporting cascade (24h/72h/14d) to ENISA SRP.
// Incidents are scoped to a CRA project; results of the cascade are exported as a
// structured draft (no live ENISA API yet).
export interface Deadline {
key: string
label: string
article: string
due_at: string | null
submitted_at: string | null
status: 'submitted' | 'overdue' | 'due_soon' | 'pending'
remaining_seconds: number | null
}
export interface Incident {
id: string
status: string
summary: string
product_name?: string
product_version?: string
manufacturer?: string
kind?: string
severity?: string
aware_at?: string
contact?: string
impact?: string
root_cause?: string
corrective_measures?: string
patch_available?: boolean
personal_data_affected?: boolean
deadlines: Deadline[]
next_stage: string | null
submissions?: Record<string, { submitted_at: string; report: Record<string, unknown> }>
}
export interface Meta {
stages: { key: string; label: string; article: string; hours: number }[]
severities: string[]
kinds: string[]
reporting_active_from: string
}
interface Project { id: string; name: string }
const j = (r: Response) => (r.ok ? r.json() : Promise.reject(new Error(`HTTP ${r.status}`)))
export function useMeldewesen() {
const [meta, setMeta] = useState<Meta | null>(null)
const [projects, setProjects] = useState<Project[]>([])
const [projectId, setProjectId] = useState('')
const [incidents, setIncidents] = useState<Incident[]>([])
const [loading, setLoading] = useState(false)
const [error, setError] = useState<string | null>(null)
useEffect(() => {
fetch('/api/sdk/v1/cra/incidents/meta').then(j).then(setMeta).catch(() => {})
fetch('/api/sdk/v1/cra/projects')
.then(j)
.then((d) => {
const list: Project[] = (d.projects || d || []).map((p: any) => ({ id: p.id, name: p.name || p.machine_name || p.id }))
setProjects(list)
if (list.length && !projectId) setProjectId(list[0].id)
})
.catch(() => {})
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
const loadIncidents = useCallback((pid: string) => {
if (!pid) { setIncidents([]); return }
setLoading(true)
setError(null)
fetch(`/api/sdk/v1/cra/incidents?cra_project_id=${encodeURIComponent(pid)}`)
.then(j)
.then((d) => setIncidents(d.incidents || []))
.catch((e) => setError(String(e?.message || e)))
.finally(() => setLoading(false))
}, [])
useEffect(() => { loadIncidents(projectId) }, [projectId, loadIncidents])
const createIncident = useCallback(async (body: Record<string, unknown>) => {
const r = await fetch('/api/sdk/v1/cra/incidents', {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ ...body, cra_project_id: projectId }),
})
if (r.ok) loadIncidents(projectId)
return r.ok
}, [projectId, loadIncidents])
const submitStage = useCallback(async (incidentId: string, stage: string) => {
const r = await fetch(`/api/sdk/v1/cra/incidents/${incidentId}/submit/${stage}`, { method: 'POST' })
if (r.ok) loadIncidents(projectId)
return r.ok
}, [projectId, loadIncidents])
const patchIncident = useCallback(async (incidentId: string, patch: Record<string, unknown>) => {
const r = await fetch(`/api/sdk/v1/cra/incidents/${incidentId}`, {
method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(patch),
})
if (r.ok) loadIncidents(projectId)
return r.ok
}, [projectId, loadIncidents])
return {
meta, projects, projectId, setProjectId, incidents, loading, error,
createIncident, submitStage, patchIncident, reload: () => loadIncidents(projectId),
}
}
// Download the ENISA export draft for a stage as JSON.
export async function downloadStageExport(incidentId: string, stage: string, summary: string): Promise<void> {
const r = await fetch(`/api/sdk/v1/cra/incidents/${incidentId}/export/${stage}`)
if (!r.ok) return
const data = await r.json()
const safe = (summary || 'meldung').replace(/[^\w\-äöüÄÖÜß ]/g, '').trim().replace(/\s+/g, '_').slice(0, 40)
const blob = new Blob([JSON.stringify(data.report, null, 2)], { type: 'application/json' })
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = `ENISA_${stage}_${safe || 'meldung'}.json`
document.body.appendChild(a)
a.click()
a.remove()
URL.revokeObjectURL(url)
}