Enterprise-Backup-, Recovery-, Verification-, Security- und Monitoring-Plattform fuer Proxmox VE, Windows, Linux und Dateisysteme. Der Leitsatz, der fast jede Entscheidung erklaert: Ein Backup gilt erst als vertrauenswuerdig, wenn Integritaet geprueft und Wiederherstellbarkeit nachgewiesen wurde. Deshalb steigt ein Wiederherstellungspunkt erst nach einem tatsaechlich durchgefuehrten Restore-Test auf "recoverable", und Unbekanntes geht in keine Bewertung als "gut" ein. Umfang (Phasen 0-23): - Repository Engine: inhaltsadressierte Bloecke, atomares Commit-Protokoll, Katalogaufbau allein aus den Manifesten — ohne Datenbank - Backup Engine: inhaltsabhaengiges Chunking, Deduplizierung trotz Verschluesselung, zstd, AES-256-GCM, Streaming mit Gegendruck - Agenten fuer Windows und Linux mit Auftragsabholung (Pull-Modell) - Proxmox-Provider mit beiden Zugriffswegen auf die Sicherungsarchive - Scheduler, Recovery Engine mit Pruefpunkt, Verification, Unveraenderlichkeit - Weboberflaeche, Kennzahlen, Meldungen, Berichte, Security Center, Ransomware-Heuristik (meldet, handelt nie) - Disaster Recovery, Haertung, Leistungsmessung, Chaos Testing - Eingefrorene Vertraege fuer API, Migrationen, Backup-Format und Repository - Auslieferungspaket fuer linux/amd64, linux/arm64 und windows/amd64 Nicht enthalten und als solches gekennzeichnet: Kapazitaetsprognose, Backup Copy, Changed Block Tracking bei Proxmox, erweiterte Attribute und ACLs. Gebaut, aber nie auf echter Hardware gefahren: der Windows-Dienst, die systemd-Einheit und der verpflichtende Proxmox-Meilenstein — ob eine wiederhergestellte VM startet, ist ungeprueft. Einzelheiten in CHANGELOG.md und docs/release-candidate.md. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
306 lines
9.1 KiB
TypeScript
306 lines
9.1 KiB
TypeScript
/**
|
|
* 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<SecurityResponse>('/security', { signal: abortSignal }),
|
|
[],
|
|
);
|
|
|
|
const { loadState, data, loadError, reload } = useApiResource<SecurityResponse>(loadAssessment);
|
|
|
|
return (
|
|
<section className="page">
|
|
<header className="page__header">
|
|
<h1 className="page__title">Sicherheit</h1>
|
|
</header>
|
|
|
|
{loadState === 'loading' && <LoadingState what="Die Sicherheitslage" />}
|
|
{loadState === 'failed' && loadError !== null && (
|
|
<ErrorState error={loadError} onRetry={reload} />
|
|
)}
|
|
|
|
{loadState === 'loaded' && data !== null && (
|
|
<>
|
|
<ScoreHeader response={data} />
|
|
<FindingList assessment={data.assessment} />
|
|
<AreaTable assessment={data.assessment} />
|
|
</>
|
|
)}
|
|
</section>
|
|
);
|
|
}
|
|
|
|
/** 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 (
|
|
<div className={`score-card score-card--${gradeClassOf(assessment)}`}>
|
|
<div className="score-card__figure">
|
|
<span className="score-card__value">{assessment.score}</span>
|
|
<span className="score-card__unit">%</span>
|
|
</div>
|
|
|
|
<div className="score-card__text">
|
|
<p className="score-card__grade">{assessment.grade}</p>
|
|
<p className="score-card__summary">{assessment.summary}</p>
|
|
|
|
{/* Die nicht gepruefte Bereiche stehen namentlich da: Sie sind die
|
|
Handlungsanweisung, nicht nur eine Einschraenkung. */}
|
|
{assessment.unchecked_areas.length > 0 && (
|
|
<p className="score-card__unchecked">
|
|
Nicht geprueft: {assessment.unchecked_areas.join(', ')}
|
|
</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
/** 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 (
|
|
<p className="page__state page__state--empty">
|
|
Keine Befunde in den geprueften Bereichen.
|
|
</p>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<ul className="finding-list">
|
|
{orderedFindings.map((finding) => (
|
|
<li className={`finding finding--${finding.severity}`} key={`${finding.area}-${finding.title}`}>
|
|
<div className="finding__head">
|
|
<span className={`badge badge--${severityClassOf(finding.severity)}`}>
|
|
{finding.severity}
|
|
</span>
|
|
<h2 className="finding__title">{finding.title}</h2>
|
|
</div>
|
|
|
|
<p className="finding__explanation">{finding.explanation}</p>
|
|
|
|
<dl className="finding__details">
|
|
<div className="finding__detail">
|
|
<dt>Betroffen</dt>
|
|
<dd>{finding.affected_object}</dd>
|
|
</div>
|
|
|
|
{/* Die Empfehlung steht hervorgehoben: Sie ist der Grund, warum der
|
|
Befund ueberhaupt angezeigt wird. */}
|
|
<div className="finding__detail finding__detail--recommendation">
|
|
<dt>Naechster Schritt</dt>
|
|
<dd>{finding.recommendation}</dd>
|
|
</div>
|
|
</dl>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
);
|
|
}
|
|
|
|
/** Eigenschaften der Bereichsuebersicht. */
|
|
interface AreaTableProperties {
|
|
/** Die Sicherheitslage. */
|
|
readonly assessment: SecurityAssessment;
|
|
}
|
|
|
|
/** Zeigt die Ergebnisse je Pruefbereich. */
|
|
function AreaTable({ assessment }: AreaTableProperties): React.JSX.Element {
|
|
return (
|
|
<section className="area-overview">
|
|
<h2 className="area-overview__title">Geprueft wurde</h2>
|
|
|
|
<div className="table-wrapper">
|
|
<table className="data-table">
|
|
<thead>
|
|
<tr>
|
|
<th scope="col">Bereich</th>
|
|
<th scope="col">Ergebnis</th>
|
|
<th scope="col">Punkte</th>
|
|
</tr>
|
|
</thead>
|
|
|
|
<tbody>
|
|
{assessment.areas.map((areaResult) => (
|
|
<tr key={areaResult.area}>
|
|
<td>{areaResult.title}</td>
|
|
|
|
<td>
|
|
{areaResult.available ? (
|
|
areaResult.summary
|
|
) : (
|
|
<>
|
|
<span className="badge badge--neutral">nicht pruefbar</span>
|
|
<span className="data-table__secondary">{areaResult.unavailable_reason}</span>
|
|
</>
|
|
)}
|
|
</td>
|
|
|
|
<td className="data-table__number">
|
|
{areaResult.available ? (
|
|
`${areaResult.earned_points} / ${areaResult.weight}`
|
|
) : (
|
|
// Ein ungeprueft Bereich geht nicht in die Rechnung ein —
|
|
// weder positiv noch negativ.
|
|
<span className="data-table__unknown">zaehlt nicht</span>
|
|
)}
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</section>
|
|
);
|
|
}
|
|
|
|
/** 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;
|
|
}
|
|
}
|