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
- /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>
127 lines
4.5 KiB
TypeScript
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)
|
|
}
|