Files split by agents before rate limit: - dsr/api.ts (669 → barrel + helpers) - einwilligungen/context.tsx (669 → barrel + hooks/reducer) - export.ts (753 → barrel + domain exporters) - incidents/api.ts (845 → barrel + api-helpers) - tom-generator/context.tsx (720 → barrel + hooks/reducer) - vendor-compliance/context.tsx (1010 → 234 provider + hooks/reducer) - api-docs/endpoints.ts — partially split (3 domain files created) - academy/api.ts — partially split (helpers extracted) - whistleblower/api.ts — partially split (helpers extracted) next build passes. api-client.ts (885) deferred to next session. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
373 lines
12 KiB
TypeScript
373 lines
12 KiB
TypeScript
/**
|
|
* Incident CRUD, Risk Assessment, Notifications, Measures, Timeline, Statistics
|
|
*/
|
|
|
|
import {
|
|
Incident,
|
|
IncidentListResponse,
|
|
IncidentFilters,
|
|
IncidentCreateRequest,
|
|
IncidentUpdateRequest,
|
|
IncidentStatistics,
|
|
IncidentMeasure,
|
|
TimelineEntry,
|
|
RiskAssessmentRequest,
|
|
AuthorityNotification,
|
|
DataSubjectNotification,
|
|
IncidentSeverity,
|
|
IncidentStatus,
|
|
IncidentCategory,
|
|
} from './types'
|
|
|
|
import { INCIDENTS_API_BASE, fetchWithTimeout, getAuthHeaders } from './api-helpers'
|
|
|
|
// =============================================================================
|
|
// INCIDENT LIST & CRUD
|
|
// =============================================================================
|
|
|
|
/**
|
|
* Alle Vorfaelle abrufen mit optionalen Filtern
|
|
*/
|
|
export async function fetchIncidents(filters?: IncidentFilters): Promise<IncidentListResponse> {
|
|
const params = new URLSearchParams()
|
|
|
|
if (filters) {
|
|
if (filters.status) {
|
|
const statuses = Array.isArray(filters.status) ? filters.status : [filters.status]
|
|
statuses.forEach(s => params.append('status', s))
|
|
}
|
|
if (filters.severity) {
|
|
const severities = Array.isArray(filters.severity) ? filters.severity : [filters.severity]
|
|
severities.forEach(s => params.append('severity', s))
|
|
}
|
|
if (filters.category) {
|
|
const categories = Array.isArray(filters.category) ? filters.category : [filters.category]
|
|
categories.forEach(c => params.append('category', c))
|
|
}
|
|
if (filters.assignedTo) params.set('assignedTo', filters.assignedTo)
|
|
if (filters.overdue !== undefined) params.set('overdue', String(filters.overdue))
|
|
if (filters.search) params.set('search', filters.search)
|
|
if (filters.dateFrom) params.set('dateFrom', filters.dateFrom)
|
|
if (filters.dateTo) params.set('dateTo', filters.dateTo)
|
|
}
|
|
|
|
const queryString = params.toString()
|
|
const url = `${INCIDENTS_API_BASE}/api/v1/incidents${queryString ? `?${queryString}` : ''}`
|
|
|
|
return fetchWithTimeout<IncidentListResponse>(url)
|
|
}
|
|
|
|
/**
|
|
* Einzelnen Vorfall per ID abrufen
|
|
*/
|
|
export async function fetchIncident(id: string): Promise<Incident> {
|
|
return fetchWithTimeout<Incident>(`${INCIDENTS_API_BASE}/api/v1/incidents/${id}`)
|
|
}
|
|
|
|
/**
|
|
* Neuen Vorfall erstellen
|
|
*/
|
|
export async function createIncident(request: IncidentCreateRequest): Promise<Incident> {
|
|
return fetchWithTimeout<Incident>(`${INCIDENTS_API_BASE}/api/v1/incidents`, {
|
|
method: 'POST',
|
|
body: JSON.stringify(request)
|
|
})
|
|
}
|
|
|
|
/**
|
|
* Vorfall aktualisieren
|
|
*/
|
|
export async function updateIncident(id: string, update: IncidentUpdateRequest): Promise<Incident> {
|
|
return fetchWithTimeout<Incident>(`${INCIDENTS_API_BASE}/api/v1/incidents/${id}`, {
|
|
method: 'PUT',
|
|
body: JSON.stringify(update)
|
|
})
|
|
}
|
|
|
|
/**
|
|
* Vorfall loeschen (Soft Delete)
|
|
*/
|
|
export async function deleteIncident(id: string): Promise<void> {
|
|
await fetchWithTimeout<void>(`${INCIDENTS_API_BASE}/api/v1/incidents/${id}`, {
|
|
method: 'DELETE'
|
|
})
|
|
}
|
|
|
|
// =============================================================================
|
|
// RISK ASSESSMENT
|
|
// =============================================================================
|
|
|
|
/**
|
|
* Risikobewertung fuer einen Vorfall durchfuehren (Art. 33 DSGVO)
|
|
*/
|
|
export async function submitRiskAssessment(
|
|
incidentId: string,
|
|
assessment: RiskAssessmentRequest
|
|
): Promise<Incident> {
|
|
return fetchWithTimeout<Incident>(
|
|
`${INCIDENTS_API_BASE}/api/v1/incidents/${incidentId}/risk-assessment`,
|
|
{
|
|
method: 'POST',
|
|
body: JSON.stringify(assessment)
|
|
}
|
|
)
|
|
}
|
|
|
|
// =============================================================================
|
|
// AUTHORITY NOTIFICATION (Art. 33 DSGVO)
|
|
// =============================================================================
|
|
|
|
/**
|
|
* Meldeformular fuer die Aufsichtsbehoerde generieren
|
|
*/
|
|
export async function generateAuthorityForm(incidentId: string): Promise<Blob> {
|
|
const response = await fetch(
|
|
`${INCIDENTS_API_BASE}/api/v1/incidents/${incidentId}/authority-form/pdf`,
|
|
{
|
|
headers: getAuthHeaders()
|
|
}
|
|
)
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`PDF-Generierung fehlgeschlagen: ${response.statusText}`)
|
|
}
|
|
|
|
return response.blob()
|
|
}
|
|
|
|
/**
|
|
* Meldung an die Aufsichtsbehoerde einreichen (Art. 33 DSGVO)
|
|
*/
|
|
export async function submitAuthorityNotification(
|
|
incidentId: string,
|
|
data: Partial<AuthorityNotification>
|
|
): Promise<Incident> {
|
|
return fetchWithTimeout<Incident>(
|
|
`${INCIDENTS_API_BASE}/api/v1/incidents/${incidentId}/authority-notification`,
|
|
{
|
|
method: 'POST',
|
|
body: JSON.stringify(data)
|
|
}
|
|
)
|
|
}
|
|
|
|
// =============================================================================
|
|
// DATA SUBJECT NOTIFICATION (Art. 34 DSGVO)
|
|
// =============================================================================
|
|
|
|
/**
|
|
* Betroffene Personen benachrichtigen (Art. 34 DSGVO)
|
|
*/
|
|
export async function sendDataSubjectNotification(
|
|
incidentId: string,
|
|
data: Partial<DataSubjectNotification>
|
|
): Promise<Incident> {
|
|
return fetchWithTimeout<Incident>(
|
|
`${INCIDENTS_API_BASE}/api/v1/incidents/${incidentId}/data-subject-notification`,
|
|
{
|
|
method: 'POST',
|
|
body: JSON.stringify(data)
|
|
}
|
|
)
|
|
}
|
|
|
|
// =============================================================================
|
|
// MEASURES (Massnahmen)
|
|
// =============================================================================
|
|
|
|
/**
|
|
* Massnahme hinzufuegen (Sofort-, Korrektur- oder Praeventionsmassnahme)
|
|
*/
|
|
export async function addMeasure(
|
|
incidentId: string,
|
|
measure: Omit<IncidentMeasure, 'id' | 'incidentId'>
|
|
): Promise<Incident> {
|
|
return fetchWithTimeout<Incident>(
|
|
`${INCIDENTS_API_BASE}/api/v1/incidents/${incidentId}/measures`,
|
|
{
|
|
method: 'POST',
|
|
body: JSON.stringify(measure)
|
|
}
|
|
)
|
|
}
|
|
|
|
/**
|
|
* Massnahme aktualisieren
|
|
*/
|
|
export async function updateMeasure(
|
|
measureId: string,
|
|
update: Partial<IncidentMeasure>
|
|
): Promise<IncidentMeasure> {
|
|
return fetchWithTimeout<IncidentMeasure>(
|
|
`${INCIDENTS_API_BASE}/api/v1/measures/${measureId}`,
|
|
{
|
|
method: 'PUT',
|
|
body: JSON.stringify(update)
|
|
}
|
|
)
|
|
}
|
|
|
|
/**
|
|
* Massnahme als abgeschlossen markieren
|
|
*/
|
|
export async function completeMeasure(measureId: string): Promise<IncidentMeasure> {
|
|
return fetchWithTimeout<IncidentMeasure>(
|
|
`${INCIDENTS_API_BASE}/api/v1/measures/${measureId}/complete`,
|
|
{
|
|
method: 'POST'
|
|
}
|
|
)
|
|
}
|
|
|
|
// =============================================================================
|
|
// TIMELINE
|
|
// =============================================================================
|
|
|
|
/**
|
|
* Zeitleisteneintrag hinzufuegen
|
|
*/
|
|
export async function addTimelineEntry(
|
|
incidentId: string,
|
|
entry: Omit<TimelineEntry, 'id' | 'incidentId'>
|
|
): Promise<Incident> {
|
|
return fetchWithTimeout<Incident>(
|
|
`${INCIDENTS_API_BASE}/api/v1/incidents/${incidentId}/timeline`,
|
|
{
|
|
method: 'POST',
|
|
body: JSON.stringify(entry)
|
|
}
|
|
)
|
|
}
|
|
|
|
// =============================================================================
|
|
// CLOSE INCIDENT
|
|
// =============================================================================
|
|
|
|
/**
|
|
* Vorfall abschliessen mit Lessons Learned
|
|
*/
|
|
export async function closeIncident(
|
|
incidentId: string,
|
|
lessonsLearned: string
|
|
): Promise<Incident> {
|
|
return fetchWithTimeout<Incident>(
|
|
`${INCIDENTS_API_BASE}/api/v1/incidents/${incidentId}/close`,
|
|
{
|
|
method: 'POST',
|
|
body: JSON.stringify({ lessonsLearned })
|
|
}
|
|
)
|
|
}
|
|
|
|
// =============================================================================
|
|
// STATISTICS
|
|
// =============================================================================
|
|
|
|
/**
|
|
* Vorfall-Statistiken abrufen
|
|
*/
|
|
export async function fetchIncidentStatistics(): Promise<IncidentStatistics> {
|
|
return fetchWithTimeout<IncidentStatistics>(
|
|
`${INCIDENTS_API_BASE}/api/v1/incidents/statistics`
|
|
)
|
|
}
|
|
|
|
// =============================================================================
|
|
// SDK PROXY FUNCTION (mit Fallback auf Mock-Daten)
|
|
// =============================================================================
|
|
|
|
/**
|
|
* Fetch Incident-Liste via SDK-Proxy mit Fallback auf Mock-Daten
|
|
*/
|
|
export async function fetchSDKIncidentList(): Promise<{ incidents: Incident[]; statistics: IncidentStatistics }> {
|
|
try {
|
|
const res = await fetch('/api/sdk/v1/incidents', {
|
|
headers: getAuthHeaders()
|
|
})
|
|
if (!res.ok) {
|
|
throw new Error(`HTTP ${res.status}`)
|
|
}
|
|
const data = await res.json()
|
|
const incidents: Incident[] = data.incidents || []
|
|
|
|
const statistics = computeStatistics(incidents)
|
|
return { incidents, statistics }
|
|
} catch (error) {
|
|
console.warn('SDK-Backend nicht erreichbar, verwende Mock-Daten:', error)
|
|
// Import mock data lazily to keep this file lean
|
|
const { createMockIncidents, createMockStatistics } = await import('./api-mock')
|
|
const incidents = createMockIncidents()
|
|
const statistics = createMockStatistics()
|
|
return { incidents, statistics }
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Statistiken lokal aus Incident-Liste berechnen
|
|
*/
|
|
function computeStatistics(incidents: Incident[]): IncidentStatistics {
|
|
const countBy = <K extends string>(items: { [key: string]: unknown }[], field: string): Record<K, number> => {
|
|
const result: Record<string, number> = {}
|
|
items.forEach(item => {
|
|
const key = String(item[field])
|
|
result[key] = (result[key] || 0) + 1
|
|
})
|
|
return result as Record<K, number>
|
|
}
|
|
|
|
const statusCounts = countBy<IncidentStatus>(incidents as unknown as { [key: string]: unknown }[], 'status')
|
|
const severityCounts = countBy<IncidentSeverity>(incidents as unknown as { [key: string]: unknown }[], 'severity')
|
|
const categoryCounts = countBy<IncidentCategory>(incidents as unknown as { [key: string]: unknown }[], 'category')
|
|
|
|
const openIncidents = incidents.filter(i => i.status !== 'closed').length
|
|
const notificationsPending = incidents.filter(i =>
|
|
i.authorityNotification !== null &&
|
|
i.authorityNotification.status === 'pending' &&
|
|
i.status !== 'closed'
|
|
).length
|
|
|
|
let totalResponseHours = 0
|
|
let respondedCount = 0
|
|
incidents.forEach(i => {
|
|
if (i.riskAssessment && i.riskAssessment.assessedAt) {
|
|
const detected = new Date(i.detectedAt).getTime()
|
|
const assessed = new Date(i.riskAssessment.assessedAt).getTime()
|
|
totalResponseHours += (assessed - detected) / (1000 * 60 * 60)
|
|
respondedCount++
|
|
}
|
|
})
|
|
|
|
return {
|
|
totalIncidents: incidents.length,
|
|
openIncidents,
|
|
notificationsPending,
|
|
averageResponseTimeHours: respondedCount > 0 ? Math.round(totalResponseHours / respondedCount * 10) / 10 : 0,
|
|
bySeverity: {
|
|
low: severityCounts['low'] || 0,
|
|
medium: severityCounts['medium'] || 0,
|
|
high: severityCounts['high'] || 0,
|
|
critical: severityCounts['critical'] || 0
|
|
},
|
|
byCategory: {
|
|
data_breach: categoryCounts['data_breach'] || 0,
|
|
unauthorized_access: categoryCounts['unauthorized_access'] || 0,
|
|
data_loss: categoryCounts['data_loss'] || 0,
|
|
system_compromise: categoryCounts['system_compromise'] || 0,
|
|
phishing: categoryCounts['phishing'] || 0,
|
|
ransomware: categoryCounts['ransomware'] || 0,
|
|
insider_threat: categoryCounts['insider_threat'] || 0,
|
|
physical_breach: categoryCounts['physical_breach'] || 0,
|
|
other: categoryCounts['other'] || 0
|
|
},
|
|
byStatus: {
|
|
detected: statusCounts['detected'] || 0,
|
|
assessment: statusCounts['assessment'] || 0,
|
|
containment: statusCounts['containment'] || 0,
|
|
notification_required: statusCounts['notification_required'] || 0,
|
|
notification_sent: statusCounts['notification_sent'] || 0,
|
|
remediation: statusCounts['remediation'] || 0,
|
|
closed: statusCounts['closed'] || 0
|
|
}
|
|
}
|
|
}
|