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>
218 lines
7.4 KiB
Go
218 lines
7.4 KiB
Go
// Package security beurteilt die Sicherheitslage der Anlage.
|
|
//
|
|
// Die Regel dieses Pakets steht in SYNCOVA_IMPLEMENTATION_PLAN.md §17 und ist
|
|
// ungewoehnlich konkret: **Jeder Befund traegt Schweregrad, Erklaerung,
|
|
// betroffenes Objekt und Empfehlung.**
|
|
//
|
|
// Der letzte Punkt ist der wichtigste. Ein Befund ohne Empfehlung ist eine
|
|
// Beunruhigung: Er sagt, dass etwas nicht stimmt, und laesst den Betreiber damit
|
|
// allein. Wer „MFA fehlt" liest, weiss noch nicht, ob er es selbst einrichten
|
|
// kann oder einen Administrator braucht.
|
|
package security
|
|
|
|
import (
|
|
"fmt"
|
|
"time"
|
|
)
|
|
|
|
// Severity ist der Schweregrad eines Befunds.
|
|
type Severity string
|
|
|
|
const (
|
|
// SeverityInformation ist eine Auskunft ohne Handlungsbedarf.
|
|
SeverityInformation Severity = "information"
|
|
// SeverityWarning verlangt einen Blick bei Gelegenheit.
|
|
SeverityWarning Severity = "warning"
|
|
// SeverityHigh verlangt eine Aenderung.
|
|
SeverityHigh Severity = "high"
|
|
// SeverityCritical bedeutet: Die Anlage ist so nicht sicher zu betreiben.
|
|
SeverityCritical Severity = "critical"
|
|
)
|
|
|
|
// Rank liefert die Ordnungszahl eines Schweregrads.
|
|
func (severity Severity) Rank() int {
|
|
switch severity {
|
|
case SeverityCritical:
|
|
return 4
|
|
case SeverityHigh:
|
|
return 3
|
|
case SeverityWarning:
|
|
return 2
|
|
case SeverityInformation:
|
|
return 1
|
|
default:
|
|
return 0
|
|
}
|
|
}
|
|
|
|
// Bezeichner der Pruefbereiche (SYNCOVA_IMPLEMENTATION_PLAN.md §17).
|
|
const (
|
|
// AreaMultiFactor prueft den zweiten Faktor der Konten.
|
|
AreaMultiFactor = "mfa"
|
|
// AreaEncryption prueft die Verschluesselung der Backups.
|
|
AreaEncryption = "encryption"
|
|
// AreaImmutability prueft den Loeschschutz der Repositories.
|
|
AreaImmutability = "immutability"
|
|
// AreaOffsite prueft die Kopie an einem zweiten Ort.
|
|
AreaOffsite = "offsite"
|
|
// AreaAudit prueft das Auditprotokoll.
|
|
AreaAudit = "audit"
|
|
// AreaCertificates prueft die Zertifikate der Agenten.
|
|
AreaCertificates = "certificates"
|
|
// AreaAgentSecurity prueft die Absicherung der Agenten.
|
|
AreaAgentSecurity = "agent_security"
|
|
// AreaUpdates prueft den Versionsstand.
|
|
AreaUpdates = "updates"
|
|
// AreaRansomwareRisk prueft die Widerstandsfaehigkeit gegen Verschluesselung.
|
|
AreaRansomwareRisk = "ransomware_risk"
|
|
// AreaAccessControl prueft die Verteilung destruktiver Rechte.
|
|
AreaAccessControl = "access_control"
|
|
)
|
|
|
|
// Finding ist ein einzelner Sicherheitsbefund.
|
|
//
|
|
// Die vier Pflichtfelder aus dem Plan sind nicht optional: Ein Befund ohne
|
|
// Empfehlung waere ein Alarm ohne Ausweg, und ein Befund ohne benanntes Objekt
|
|
// liesse den Betreiber suchen.
|
|
type Finding struct {
|
|
// Area ist der Pruefbereich.
|
|
Area string `json:"area"`
|
|
// Severity ist der Schweregrad.
|
|
Severity Severity `json:"severity"`
|
|
// Title ist die Ueberschrift.
|
|
Title string `json:"title"`
|
|
// Explanation erklaert, warum der Zustand ein Problem ist.
|
|
//
|
|
// Nicht „MFA fehlt", sondern warum das zaehlt: Ein gestohlenes Passwort
|
|
// genuegt dann, um Backups zu loeschen.
|
|
Explanation string `json:"explanation"`
|
|
// AffectedObject benennt das betroffene Objekt.
|
|
AffectedObject string `json:"affected_object"`
|
|
// Recommendation nennt die naechste Handlung.
|
|
Recommendation string `json:"recommendation"`
|
|
}
|
|
|
|
// Validate prueft einen Befund auf Vollstaendigkeit.
|
|
//
|
|
// Die Pruefung existiert, weil ein unvollstaendiger Befund erst im Betrieb
|
|
// auffaellt — dann, wenn jemand ratlos vor „Sicherheitsproblem erkannt" steht.
|
|
func (finding Finding) Validate() error {
|
|
if finding.Title == "" {
|
|
return fmt.Errorf("dem befund im bereich %s fehlt die ueberschrift", finding.Area)
|
|
}
|
|
|
|
if finding.Explanation == "" {
|
|
return fmt.Errorf("dem befund %q fehlt die erklaerung", finding.Title)
|
|
}
|
|
|
|
if finding.AffectedObject == "" {
|
|
return fmt.Errorf("dem befund %q fehlt das betroffene objekt", finding.Title)
|
|
}
|
|
|
|
if finding.Recommendation == "" {
|
|
return fmt.Errorf("dem befund %q fehlt die empfehlung", finding.Title)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// AreaResult ist das Ergebnis eines Pruefbereichs.
|
|
type AreaResult struct {
|
|
// Area ist der Bezeichner des Bereichs.
|
|
Area string `json:"area"`
|
|
// Title ist die Bezeichnung.
|
|
Title string `json:"title"`
|
|
// Available meldet, ob der Bereich geprueft werden konnte.
|
|
//
|
|
// Der Unterschied zu „keine Befunde" ist der Kern der ehrlichen Bewertung:
|
|
// „Wir haben nichts gefunden" und „wir haben nicht nachgesehen" sind zwei
|
|
// verschiedene Aussagen, und nur die erste ist beruhigend.
|
|
Available bool `json:"available"`
|
|
// UnavailableReason erklaert einen nicht pruefbaren Bereich.
|
|
UnavailableReason string `json:"unavailable_reason,omitempty"`
|
|
// Findings sind die Befunde des Bereichs.
|
|
Findings []Finding `json:"findings"`
|
|
// Summary fasst den Bereich in einem Satz zusammen.
|
|
Summary string `json:"summary"`
|
|
// Weight ist das Gewicht des Bereichs in der Gesamtbewertung.
|
|
Weight int `json:"weight"`
|
|
// EarnedPoints sind die erreichten Punkte.
|
|
EarnedPoints int `json:"earned_points"`
|
|
}
|
|
|
|
// WorstSeverity liefert den hoechsten Schweregrad des Bereichs.
|
|
func (result *AreaResult) WorstSeverity() Severity {
|
|
worstSeverity := Severity("")
|
|
|
|
for _, finding := range result.Findings {
|
|
if finding.Severity.Rank() > worstSeverity.Rank() {
|
|
worstSeverity = finding.Severity
|
|
}
|
|
}
|
|
|
|
return worstSeverity
|
|
}
|
|
|
|
// Assessment ist die vollstaendige Sicherheitslage.
|
|
type Assessment struct {
|
|
// Score ist die Gesamtbewertung in Prozent.
|
|
Score int `json:"score"`
|
|
// MaximumScore sind die erreichbaren Punkte der gepruefte Bereiche.
|
|
MaximumScore int `json:"maximum_score"`
|
|
// Grade ist die Einstufung der Lage.
|
|
Grade string `json:"grade"`
|
|
// Areas sind die Ergebnisse je Bereich.
|
|
Areas []AreaResult `json:"areas"`
|
|
// UncheckedAreaCount ist die Zahl nicht pruefbarer Bereiche.
|
|
//
|
|
// Sie steht neben der Prozentzahl, damit niemand eine Bewertung fuer
|
|
// belastbar haelt, die auf lauter Ungeprueftem beruht — dieselbe
|
|
// Ueberlegung wie bei der Recovery Assurance (Phase 10).
|
|
UncheckedAreaCount int `json:"unchecked_area_count"`
|
|
// CriticalFindingCount ist die Zahl kritischer Befunde.
|
|
CriticalFindingCount int `json:"critical_finding_count"`
|
|
// HighFindingCount ist die Zahl ernster Befunde.
|
|
HighFindingCount int `json:"high_finding_count"`
|
|
// Summary fasst die Lage in einem Satz zusammen.
|
|
Summary string `json:"summary"`
|
|
// UncheckedAreas nennt die nicht gepruefte Bereiche als Handlungsanweisung.
|
|
UncheckedAreas []string `json:"unchecked_areas"`
|
|
// EvaluatedAt ist der Zeitpunkt der Bewertung in UTC.
|
|
EvaluatedAt time.Time `json:"evaluated_at"`
|
|
}
|
|
|
|
// AllFindings liefert alle Befunde nach Schweregrad geordnet.
|
|
func (assessment *Assessment) AllFindings() []Finding {
|
|
allFindings := make([]Finding, 0, 16)
|
|
|
|
// Die Reihenfolge folgt dem Schweregrad, nicht dem Bereich: Wer die Liste
|
|
// oeffnet, soll oben lesen, was zuerst zu tun ist.
|
|
for _, severityLevel := range []Severity{
|
|
SeverityCritical, SeverityHigh, SeverityWarning, SeverityInformation,
|
|
} {
|
|
for _, areaResult := range assessment.Areas {
|
|
for _, finding := range areaResult.Findings {
|
|
if finding.Severity == severityLevel {
|
|
allFindings = append(allFindings, finding)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return allFindings
|
|
}
|
|
|
|
// IsTrustworthy meldet eine Bewertung, die auf genug Pruefungen beruht.
|
|
//
|
|
// Mehr als zwei ungeprueft Bereiche machen die Prozentzahl zur Vermutung. Ein
|
|
// kritischer Befund schliesst Vertrauenswuerdigkeit ohnehin aus — dieselbe
|
|
// Regel wie bei der Recovery Assurance, wo ein beschaedigtes Backup nie „gut"
|
|
// sein kann.
|
|
func (assessment *Assessment) IsTrustworthy() bool {
|
|
if assessment.CriticalFindingCount > 0 {
|
|
return false
|
|
}
|
|
|
|
return assessment.UncheckedAreaCount <= 2
|
|
}
|