/** * Security Center. * * Die Seite, die in Phase 12 als „noch nicht verfuegbar" stand — mit dem * Hinweis, die Einzelangaben seien vorhanden, aber keine Gesamtbewertung. Jetzt * gibt es beides. * * Der Aufbau folgt der Regel aus dem Plan (§17): Jeder Befund traegt * Schweregrad, Erklaerung, betroffenes Objekt und Empfehlung. Die Empfehlung ist * der wichtigste Teil — ein Befund ohne sie ist eine Beunruhigung. */ import { useCallback } from 'react'; import { requestApi } from '../../api/client'; import { useApiResource } from '../../api/useApiResource'; import { ErrorState, LoadingState } from '../../components/PageState'; /** Schweregrad eines Befunds. */ type FindingSeverity = 'information' | 'warning' | 'high' | 'critical'; /** Ein Sicherheitsbefund. */ interface SecurityFinding { /** Pruefbereich. */ readonly area: string; /** Schweregrad. */ readonly severity: FindingSeverity; /** Ueberschrift. */ readonly title: string; /** Erklaerung, warum der Zustand ein Problem ist. */ readonly explanation: string; /** Das betroffene Objekt. */ readonly affected_object: string; /** Die naechste Handlung. */ readonly recommendation: string; } /** Ergebnis eines Pruefbereichs. */ interface AreaResult { /** Bezeichner. */ readonly area: string; /** Bezeichnung. */ readonly title: string; /** Meldet, ob der Bereich geprueft werden konnte. */ readonly available: boolean; /** Erklaert einen nicht pruefbaren Bereich. */ readonly unavailable_reason?: string; /** Befunde des Bereichs. */ readonly findings: readonly SecurityFinding[]; /** Zusammenfassung. */ readonly summary: string; /** Gewicht in der Gesamtbewertung. */ readonly weight: number; /** Erreichte Punkte. */ readonly earned_points: number; } /** Die Sicherheitslage. */ interface SecurityAssessment { /** Gesamtbewertung in Prozent. */ readonly score: number; /** Erreichbare Punkte der geprueften Bereiche. */ readonly maximum_score: number; /** Einstufung. */ readonly grade: string; /** Ergebnisse je Bereich. */ readonly areas: readonly AreaResult[]; /** Zahl nicht pruefbarer Bereiche. */ readonly unchecked_area_count: number; /** Zahl kritischer Befunde. */ readonly critical_finding_count: number; /** Zahl ernster Befunde. */ readonly high_finding_count: number; /** Zusammenfassung. */ readonly summary: string; /** Nicht gepruefte Bereiche. */ readonly unchecked_areas: readonly string[]; } /** Antwort des Security Center. */ interface SecurityResponse { /** Die Sicherheitslage. */ readonly assessment: SecurityAssessment; /** Meldet eine belastbare Bewertung. */ readonly is_trustworthy: boolean; } /** Zeigt das Security Center. */ export function SecurityPage(): React.JSX.Element { const loadAssessment = useCallback( (abortSignal: AbortSignal) => requestApi('/security', { signal: abortSignal }), [], ); const { loadState, data, loadError, reload } = useApiResource(loadAssessment); return (

Sicherheit

{loadState === 'loading' && } {loadState === 'failed' && loadError !== null && ( )} {loadState === 'loaded' && data !== null && ( <> )}
); } /** Eigenschaften der Gesamtbewertung. */ interface ScoreHeaderProperties { /** Die Antwort des Security Center. */ readonly response: SecurityResponse; } /** Zeigt die Gesamtbewertung. */ function ScoreHeader({ response }: ScoreHeaderProperties): React.JSX.Element { const { assessment } = response; return (
{assessment.score} %

{assessment.grade}

{assessment.summary}

{/* Die nicht gepruefte Bereiche stehen namentlich da: Sie sind die Handlungsanweisung, nicht nur eine Einschraenkung. */} {assessment.unchecked_areas.length > 0 && (

Nicht geprueft: {assessment.unchecked_areas.join(', ')}

)}
); } /** Eigenschaften der Befundliste. */ interface FindingListProperties { /** Die Sicherheitslage. */ readonly assessment: SecurityAssessment; } /** Zeigt die Befunde nach Schweregrad geordnet. */ function FindingList({ assessment }: FindingListProperties): React.JSX.Element { const allFindings = assessment.areas.flatMap((areaResult) => areaResult.findings); const orderedFindings = [...allFindings].sort( (firstFinding, secondFinding) => severityRankOf(secondFinding.severity) - severityRankOf(firstFinding.severity), ); if (orderedFindings.length === 0) { return (

Keine Befunde in den geprueften Bereichen.

); } return ( ); } /** Eigenschaften der Bereichsuebersicht. */ interface AreaTableProperties { /** Die Sicherheitslage. */ readonly assessment: SecurityAssessment; } /** Zeigt die Ergebnisse je Pruefbereich. */ function AreaTable({ assessment }: AreaTableProperties): React.JSX.Element { return (

Geprueft wurde

{assessment.areas.map((areaResult) => ( ))}
Bereich Ergebnis Punkte
{areaResult.title} {areaResult.available ? ( areaResult.summary ) : ( <> nicht pruefbar {areaResult.unavailable_reason} )} {areaResult.available ? ( `${areaResult.earned_points} / ${areaResult.weight}` ) : ( // Ein ungeprueft Bereich geht nicht in die Rechnung ein — // weder positiv noch negativ. zaehlt nicht )}
); } /** Bildet die Einstufung auf eine Statusfarbe ab. */ function gradeClassOf(assessment: SecurityAssessment): string { if (assessment.critical_finding_count > 0) { return 'critical'; } if (assessment.score >= 90) { return 'healthy'; } if (assessment.score >= 70) { return 'warning'; } return 'high'; } /** Bildet einen Schweregrad auf eine Statusfarbe ab. */ function severityClassOf(severity: FindingSeverity): string { switch (severity) { case 'critical': return 'critical'; case 'high': return 'high'; case 'warning': return 'warning'; default: return 'info'; } } /** Liefert die Ordnungszahl eines Schweregrads. */ function severityRankOf(severity: FindingSeverity): number { switch (severity) { case 'critical': return 4; case 'high': return 3; case 'warning': return 2; default: return 1; } }